├── token_metadatas_cache └── .keep ├── .prettierrc ├── images └── empty.gif ├── platform_images ├── NLL.png ├── BLUR.png ├── NFTX.png ├── X2Y2.png ├── AUCTION.png ├── FLYWHEEL.png ├── OPENSEA.png ├── RARIBLE.png ├── ETHERSCAN.png ├── LOOKSRARE.png ├── OPENSEA_PRO.png └── SUDOSWAP.jpeg ├── client ├── fonts │ ├── RetroComputer.woff │ ├── RetroComputer.woff2 │ ├── stylesheet.css │ └── demo.html ├── twitter │ └── index.html ├── polls │ └── index.html └── index.html ├── tsconfig.build.json ├── src ├── utils │ └── array.utils.ts ├── extensions │ ├── phunks.erc721.specialised.service │ │ ├── fonts │ │ │ └── retro-computer.ttf │ │ ├── images │ │ │ ├── logo.svg │ │ │ └── logo-phunk.svg │ │ └── phunks.erc721.specialised.service.ts │ ├── dao │ │ ├── errors.ts │ │ ├── models.ts │ │ ├── crypto.ts │ │ └── dao.controller.ts │ ├── phunks.auction.flywheel.cli.extension.ts │ ├── phunks.auction.house.cli.extension.ts │ ├── phunks.auction.flywheel.extension.service.ts │ ├── phunks.gif.twitter.extension.service.ts │ ├── phunks.bid.extension.service.ts │ └── phunks.auction.house.extension.service.ts ├── parsers │ ├── parser.definition.ts │ ├── notlarvalabs.parser.ts │ ├── blur.io.basic.parser.ts │ ├── looksrare.parser.ts │ ├── cargo.parser.ts │ ├── looksrare.v2.parser.ts │ ├── x2y2.parser.ts │ ├── rarible.parser.ts │ ├── opensea.wyvern.parser.ts │ ├── blur.io.sales.parser.ts │ ├── blur.io.sweep.parser.ts │ ├── opensea.seaport.parser.ts │ └── nftx.parser.ts ├── main.ts ├── fiat-symobols.json ├── test.ts ├── logging.utils.ts ├── abi │ ├── x2y2ABI.json │ ├── notlarvalabs.json │ ├── erc721.json │ ├── nftxABI.json │ ├── punks.json │ └── phunkAuctionHouse.json ├── app.module.ts ├── clients │ ├── twitter.ts │ └── discord.ts ├── cli.module.ts ├── config.ts ├── erc721sales.service.ts └── base.service.ts ├── nest-cli.json ├── example.env ├── .github └── workflows │ ├── tests.yml │ └── publish-coverage.yml ├── tsconfig.json ├── .eslintrc.js ├── .gitignore ├── package.json ├── README.md └── LICENSE /token_metadatas_cache/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /images/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/images/empty.gif -------------------------------------------------------------------------------- /platform_images/NLL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/NLL.png -------------------------------------------------------------------------------- /platform_images/BLUR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/BLUR.png -------------------------------------------------------------------------------- /platform_images/NFTX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/NFTX.png -------------------------------------------------------------------------------- /platform_images/X2Y2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/X2Y2.png -------------------------------------------------------------------------------- /platform_images/AUCTION.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/AUCTION.png -------------------------------------------------------------------------------- /platform_images/FLYWHEEL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/FLYWHEEL.png -------------------------------------------------------------------------------- /platform_images/OPENSEA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/OPENSEA.png -------------------------------------------------------------------------------- /platform_images/RARIBLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/RARIBLE.png -------------------------------------------------------------------------------- /client/fonts/RetroComputer.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/client/fonts/RetroComputer.woff -------------------------------------------------------------------------------- /platform_images/ETHERSCAN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/ETHERSCAN.png -------------------------------------------------------------------------------- /platform_images/LOOKSRARE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/LOOKSRARE.png -------------------------------------------------------------------------------- /platform_images/OPENSEA_PRO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/OPENSEA_PRO.png -------------------------------------------------------------------------------- /platform_images/SUDOSWAP.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/platform_images/SUDOSWAP.jpeg -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /client/fonts/RetroComputer.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/client/fonts/RetroComputer.woff2 -------------------------------------------------------------------------------- /src/utils/array.utils.ts: -------------------------------------------------------------------------------- 1 | function onlyUnique(value, index, array) { 2 | return array.indexOf(value) === index; 3 | } 4 | 5 | export function unique(array:any[]) { 6 | return array.filter(onlyUnique) 7 | } -------------------------------------------------------------------------------- /src/extensions/phunks.erc721.specialised.service/fonts/retro-computer.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto-Phunks/nft-sales-twitter-bot/HEAD/src/extensions/phunks.erc721.specialised.service/fonts/retro-computer.ttf -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.ttf", "**/*.svg", "**/*.png", "**/*.json"] 6 | }, 7 | "watchAssets": true 8 | } 9 | -------------------------------------------------------------------------------- /src/parsers/parser.definition.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse } from "ethers"; 2 | 3 | export interface LogParser { 4 | platform:string 5 | parseLogs(transaction:TransactionResponse, logs:ReadonlyArray, tokenId:string):number|undefined 6 | } -------------------------------------------------------------------------------- /src/extensions/dao/errors.ts: -------------------------------------------------------------------------------- 1 | export class SignatureError extends Error { 2 | constructor(msg: string) { 3 | super(msg); 4 | 5 | // Set the prototype explicitly. 6 | Object.setPrototypeOf(this, SignatureError.prototype); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /client/fonts/stylesheet.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Retro Computer'; 3 | src: url('RetroComputer.woff2') format('woff2'), 4 | url('RetroComputer.woff') format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | TWITTER_CLIENT_ID="" 2 | TWITTER_CLIENT_SECRET="" 3 | TWITTER_ACCESS_TOKEN_KEY="" 4 | TWITTER_ACCESS_TOKEN_SECRET="" 5 | TWITTER_API_KEY="" 6 | TWITTER_API_KEY_SECRET="" 7 | TWITTER_API_BEARER_TOKEN="" 8 | 9 | ALCHEMY_API_KEY="" 10 | DISCORD_TOKEN="" 11 | 12 | GETH_NODE_ENDPOINT="" 13 | GETH_NODE_ENDPOINT_HTTP="" 14 | DEBUG_MODE="" 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [pull_request,workflow_dispatch] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - name: Install modules 9 | run: npm ci 10 | - name: Run tests 11 | run: npm run test 12 | env: 13 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 14 | -------------------------------------------------------------------------------- /src/fiat-symobols.json: -------------------------------------------------------------------------------- 1 | { 2 | "eth": { 3 | "symbol": "Ξ" 4 | }, 5 | "usd": { 6 | "symbol": "$" 7 | }, 8 | "cad": { 9 | "symbol": "$" 10 | }, 11 | "eur": { 12 | "symbol": "€" 13 | }, 14 | "gbp": { 15 | "symbol": "£" 16 | }, 17 | "aud": { 18 | "symbol": "$" 19 | }, 20 | "cny": { 21 | "symbol": "¥" 22 | }, 23 | "jpy": { 24 | "symbol": "¥" 25 | } 26 | } -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider, Network, ethers } from "ethers"; 2 | 3 | process.env['NODE_TLS_REJECT_UNAUTHORIZED']='0' 4 | const provider = ethers.getDefaultProvider("https://geth.ef3aaeddd3281ebe.dyndns.dappnode.io") as JsonRpcProvider; 5 | 6 | 7 | provider.send("debug_traceTransaction", 8 | ['0x28b859639993604a9b6c060deddede3e63c396134640cd03a6373fdc6bb8a6eb', { 9 | tracer: 'callTracer' 10 | }]).then(r => console.log(r)) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "esModuleInterop": true, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [], 9 | root: true, 10 | env: { 11 | node: true, 12 | jest: true, 13 | }, 14 | ignorePatterns: ['.eslintrc.js'], 15 | rules: { 16 | '@typescript-eslint/interface-name-prefix': 'off', 17 | '@typescript-eslint/explicit-function-return-type': 'off', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | token_metadatas_cache/*.json 2 | *.txt 3 | bids_images 4 | db.db 5 | token_images 6 | 7 | # compiled output 8 | coverage 9 | /dist 10 | /node_modules 11 | .env 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | pnpm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | lerna-debug.log* 21 | 22 | # OS 23 | .DS_Store 24 | 25 | # Tests 26 | /coverage 27 | /.nyc_output 28 | 29 | # IDEs and editors 30 | /.idea 31 | .project 32 | .classpath 33 | .c9/ 34 | *.launch 35 | .settings/ 36 | *.sublime-workspace 37 | 38 | # IDE - VSCode 39 | .vscode/* 40 | !.vscode/settings.json 41 | !.vscode/tasks.json 42 | !.vscode/launch.json 43 | !.vscode/extensions.json 44 | auction_images 45 | wrapped_punks 46 | wrapped_punks_old 47 | new 48 | original_punks_images 49 | punks_images 50 | -------------------------------------------------------------------------------- /src/extensions/dao/models.ts: -------------------------------------------------------------------------------- 1 | export interface BindWeb3RequestDto { 2 | signature:string; 3 | account:string; 4 | discordUserId:string; 5 | discordUsername:string; 6 | discordAccessToken:string; 7 | } 8 | 9 | export interface BindTwitterRequestDto { 10 | state:string; 11 | code:string; 12 | } 13 | 14 | export interface BindTwitterResultDto { 15 | createdAt:string; 16 | id:string; 17 | name:string; 18 | username:string; 19 | accessToken:string; 20 | refreshToken:string; 21 | discordUserId?:string; 22 | } 23 | 24 | export interface DAORoleConfigurationDto { 25 | guildId:string, 26 | roleId:string, 27 | gracePeriod?:number, 28 | twitter?:any, 29 | minted?: boolean, 30 | minOwnedCount?: number, 31 | minOwnedTime?: number, 32 | specificTrait?: any, 33 | disallowAll?: boolean 34 | } -------------------------------------------------------------------------------- /src/parsers/notlarvalabs.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | 4 | export class NotLarvaLabsParser implements LogParser { 5 | 6 | platform: string = 'notlarvalabs'; 7 | 8 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 9 | const result = logs.map((log: any) => { 10 | if (log.topics[0].toLowerCase() === '0x975c7be5322a86cddffed1e3e0e55471a764ac2764d25176ceb8e17feef9392c') { 11 | const relevantData = log.data.substring(2); 12 | if (tokenId !== parseInt(log.topics[1], 16).toString()) { 13 | return 14 | } 15 | return BigInt(`0x${relevantData}`) / BigInt('1000000000000000') 16 | } 17 | }).filter(n => n !== undefined) 18 | if (result.length) { 19 | return parseFloat(result[0].toString())/1000; 20 | } 21 | return undefined 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/logging.utils.ts: -------------------------------------------------------------------------------- 1 | import winston, { format } from 'winston'; 2 | import DailyRotateFile from 'winston-daily-rotate-file' 3 | const { errors } = format; 4 | 5 | export function createLogger(service:string) { 6 | return winston.createLogger({ 7 | level: 'debug', 8 | format: winston.format.combine( 9 | errors({ stack: true }), 10 | winston.format.timestamp({ 11 | format: 'YYYY-MM-DD HH:mm:ss' 12 | }), 13 | winston.format.printf(info => `[${info.timestamp}] [${info.service}] [${info.level}]: ${info.message}`+(info.splat!==undefined?`${info.splat}`:" ")) 14 | ), 15 | defaultMeta: { service }, 16 | transports: [ 17 | new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), 18 | new DailyRotateFile({ 19 | filename: 'logs/combined-%DATE%.log', 20 | datePattern: 'YYYY-MM-DD', 21 | zippedArchive: true, 22 | maxSize: '20m', 23 | maxFiles: '14d' 24 | }), 25 | new winston.transports.Console({}), 26 | ], 27 | }); 28 | } -------------------------------------------------------------------------------- /src/extensions/dao/crypto.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | const md5 = text => { 4 | return crypto 5 | .createHash('md5') 6 | .update(text) 7 | .digest(); 8 | } 9 | 10 | export function encrypt(text, secretKey) { 11 | secretKey = md5(secretKey); 12 | secretKey = Buffer.concat([secretKey, secretKey.slice(0, 8)]); // properly expand 3DES key from 128 bit to 192 bit 13 | 14 | const cipher = crypto.createCipheriv('des-ede3', secretKey, ''); 15 | const encrypted = cipher.update(text, 'utf8', 'base64'); 16 | 17 | return encrypted + cipher.final('base64'); 18 | }; 19 | 20 | export function decrypt(encryptedBase64, secretKey) { 21 | secretKey = md5(secretKey); 22 | secretKey = Buffer.concat([secretKey, secretKey.slice(0, 8)]); // properly expand 3DES key from 128 bit to 192 bit 23 | const decipher = crypto.createDecipheriv('des-ede3', secretKey, ''); 24 | const result = Buffer.concat([decipher.update(encryptedBase64, 'base64'), decipher.final()]) 25 | return result.toString('utf8'); 26 | }; 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/parsers/blur.io.basic.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import blurABI from '../abi/blur.json'; 5 | 6 | const blurContractAddress = '0x000000000000ad05ccc4f10045630fb830b95127'; 7 | const blurInterface = new ethers.Interface(blurABI); 8 | 9 | export class BlurIOBasicParser implements LogParser { 10 | 11 | platform: string = 'blurio'; 12 | 13 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 14 | const result = logs.map((log: any) => { 15 | if (log.address.toLowerCase() === blurContractAddress.toLowerCase()) { 16 | return blurInterface.parseLog(log); 17 | } 18 | }).filter(l => l?.name === 'OrdersMatched' && l?.args.buy.tokenId.toString() === tokenId) 19 | if (result.length) { 20 | const weiValue = (result[0]?.args?.buy.price)?.toString(); 21 | const value = ethers.formatEther(weiValue); 22 | return parseFloat(value); 23 | } 24 | return undefined 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/parsers/looksrare.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import looksRareABI from '../abi/looksRareABI.json'; 5 | 6 | const looksInterface = new ethers.Interface(looksRareABI); 7 | const looksRareContractAddress = '0x59728544b08ab483533076417fbbb2fd0b17ce3a'; // Don't change unless deprecated 8 | 9 | export class LooksRareParser implements LogParser { 10 | 11 | platform: string = 'looksrare'; 12 | 13 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 14 | const result = logs.map((log: any) => { 15 | if (log.address.toLowerCase() === looksRareContractAddress.toLowerCase()) { 16 | return looksInterface.parseLog(log); 17 | } 18 | }).filter((log: any) => (log?.name === 'TakerAsk' || log?.name === 'TakerBid') && 19 | log?.args.tokenId == tokenId); 20 | if (result.length) { 21 | const weiValue = (result[0]?.args?.price)?.toString(); 22 | const value = ethers.formatEther(weiValue); 23 | return parseFloat(value) 24 | } 25 | return undefined 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/parsers/cargo.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | 5 | const cargoTopicIdentifier = '0x5535fa724c02f50c6fb4300412f937dbcdf655b0ebd4ecaca9a0d377d0c0d9cc' 6 | 7 | export class CargoParser implements LogParser { 8 | 9 | platform: string = 'cargo'; 10 | 11 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 12 | const result = logs.map((log: any) => { 13 | if (log.topics[0] === cargoTopicIdentifier) { 14 | // cargo sale 15 | const data = log.data.substring(2); 16 | const dataSlices = data.match(/.{1,64}/g); 17 | const amount = BigInt(`0x${dataSlices[15]}`); 18 | const saleTokenId = `${parseInt(dataSlices[10], 16)}`; 19 | const commission = BigInt(`0x${dataSlices[16]}`) 20 | 21 | if (saleTokenId === tokenId) 22 | return amount + commission 23 | } 24 | return undefined 25 | }).filter(r => r !== undefined) 26 | if (result.length) { 27 | return (parseFloat((result[0] / BigInt('10000000000000000')).toString())/100) 28 | } 29 | return undefined 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/parsers/looksrare.v2.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import looksRareABIv2 from '../abi/looksRareABIv2.json'; 4 | 5 | const looksRareContractAddressV2 = '0x0000000000e655fae4d56241588680f86e3b2377'; // Don't change unless deprecated 6 | const looksInterfaceV2 = new ethers.Interface(looksRareABIv2); 7 | 8 | export class LooksRareV2Parser implements LogParser { 9 | 10 | platform: string = 'looksrare'; 11 | 12 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 13 | const result = logs.map((log: any) => { 14 | if (log.address.toLowerCase() === looksRareContractAddressV2.toLowerCase()) { 15 | return looksInterfaceV2.parseLog(log); 16 | } 17 | }) 18 | .filter(log => log !== undefined) 19 | .filter((log: any) => { 20 | return (log?.name === 'TakerAsk' || log?.name === 'TakerBid') && 21 | log?.args.itemIds.map(i => i.toString()).indexOf(tokenId) > -1 22 | }); 23 | if (result.length) { 24 | const weiValue = (result[0]?.args?.feeAmounts[0])?.toString(); 25 | const value = ethers.formatEther(weiValue); 26 | return parseFloat(value) 27 | } 28 | return undefined 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/extensions/dao/dao.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import fetch from "node-fetch"; 3 | import { BindTwitterRequestDto, BindWeb3RequestDto } from './models'; 4 | import { DAOService } from './dao.extension.service'; 5 | import { SignatureError } from './errors'; 6 | import { encrypt } from './crypto'; 7 | 8 | @Controller('dao') 9 | export class DAOController { 10 | 11 | constructor(private daoService:DAOService) { 12 | 13 | } 14 | 15 | @Get('status') 16 | status(): string { 17 | return 'ok'; 18 | } 19 | 20 | @Get('polls') 21 | polls(): string { 22 | const polls = this.daoService.getAllPolls() 23 | return polls; 24 | } 25 | 26 | @Post('bind/twitter') 27 | bindTwitter(@Body() request: BindTwitterRequestDto): any { 28 | console.log(request) 29 | try { 30 | this.daoService.bindTwitterAccount(request) 31 | } catch (error) { 32 | console.log('error', error) 33 | return {result: 'ko'}; 34 | } 35 | return {result: 'ok'}; 36 | } 37 | 38 | @Post('bind/web3') 39 | bind(@Body() request: BindWeb3RequestDto): any { 40 | console.log(request) 41 | try { 42 | // TODO handle guildId 43 | this.daoService.bindWeb3Account(request) 44 | } catch (error) { 45 | console.log('error', error) 46 | return {result: 'ko'}; 47 | } 48 | return {result: 'ok'}; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/parsers/x2y2.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import looksRareABIv2 from '../abi/looksRareABIv2.json'; 4 | 5 | const looksRareContractAddressV2 = '0x0000000000e655fae4d56241588680f86e3b2377'; // Don't change unless deprecated 6 | const looksInterfaceV2 = new ethers.Interface(looksRareABIv2); 7 | 8 | export class X2Y2Parser implements LogParser { 9 | 10 | platform: string = 'x2y2'; 11 | 12 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 13 | const result = logs.map((log: any, index:number) => { 14 | if (log.topics[0].toLowerCase() === '0x3cbb63f144840e5b1b0a38a7c19211d2e89de4d7c5faf8b2d3c1776c302d1d33') { 15 | const data = log.data.substring(2); 16 | const dataSlices = data.match(/.{1,64}/g); 17 | // find the right token 18 | if (BigInt(`0x${dataSlices[18]}`).toString() !== tokenId) return; 19 | let amount = BigInt(`0x${dataSlices[12]}`) / BigInt('1000000000000000'); 20 | if (amount === BigInt(0)) { 21 | amount = BigInt(`0x${dataSlices[26]}`) / BigInt('1000000000000000'); 22 | } 23 | return amount 24 | } 25 | }).filter(n => n !== undefined) 26 | if (result.length) { 27 | return parseFloat(result[0].toString())/1000; 28 | } 29 | return undefined 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/parsers/rarible.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import looksRareABI from '../abi/looksRareABI.json'; 5 | 6 | const raribleTopicIdentifier = '0x268820db288a211986b26a8fda86b1e0046281b21206936bb0e61c67b5c79ef4' 7 | 8 | export class RaribleParser implements LogParser { 9 | 10 | platform: string = 'rarible'; 11 | 12 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 13 | const result = logs.map((log: any) => { 14 | if (log.topics[0] === raribleTopicIdentifier ) { 15 | const nftData = log.data.substring(2); 16 | const nftDataSlices = nftData.match(/.{1,64}/g); 17 | 18 | if (nftDataSlices.length !== 16) { 19 | // invalid slice 20 | return undefined 21 | } 22 | 23 | if (BigInt(`0x${nftDataSlices[12]}`).toString() !== tokenId) return; 24 | 25 | // rarible sale 26 | return BigInt(`0x${nftDataSlices[4]}`); 27 | } 28 | return undefined 29 | }).filter(r => r !== undefined) 30 | if (result.length) { 31 | const amount = result.reduce((previous,current) => previous + current, BigInt(0)); 32 | return (parseFloat((amount / BigInt('10000000000000000')).toString())/100) 33 | } 34 | return undefined 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/parsers/opensea.wyvern.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import openseaWyvernABI from '../abi/opensea_wyvern.json'; 5 | 6 | const openseaWyvernInterface = new ethers.Interface(openseaWyvernABI); 7 | 8 | export class OpenSeaWyvernParser implements LogParser { 9 | 10 | platform: string = 'opensea'; 11 | 12 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 13 | const result = logs.map((log: any) => { 14 | if (log.topics[0].toLowerCase() === '0xc4109843e0b7d514e4c093114b863f8e7d8d9a458c372cd51bfe526b588006c9') { 15 | const logDescription = openseaWyvernInterface.parseLog(log); 16 | const price = logDescription.args.price 17 | const tokenCount = logs 18 | .filter(l => l.address.toLowerCase() === config.contract_address.toLowerCase() && 19 | l.topics[0].toLowerCase() === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') 20 | .map(l => l.topics[3]) 21 | // take unique value 22 | .filter((value, index, array) => array.indexOf(value) === index) 23 | .length 24 | return ethers.formatEther(price / BigInt(tokenCount)); 25 | } 26 | }).filter(n => n !== undefined) 27 | if (result.length) return parseFloat(result[0]) 28 | return undefined 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /.github/workflows/publish-coverage.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - run: npm ci 35 | - run: npm test 36 | env: 37 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 38 | - name: Setup Pages 39 | uses: actions/configure-pages@v3 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v2 42 | with: 43 | # Upload entire repository 44 | path: './coverage' 45 | - name: Deploy to GitHub Pages 46 | id: deployment 47 | uses: actions/deploy-pages@v2 48 | -------------------------------------------------------------------------------- /src/abi/x2y2ABI.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_logic","type":"address"},{"internalType":"address","name":"admin_","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"stateMutability":"payable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"beacon","type":"address"}],"name":"BeaconUpgraded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"admin_","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newAdmin","type":"address"}],"name":"changeAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"implementation","outputs":[{"internalType":"address","name":"implementation_","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}] -------------------------------------------------------------------------------- /src/extensions/phunks.auction.flywheel.cli.extension.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { AppModule } from "../app.module"; 3 | import { exit } from "process"; 4 | import { ethers } from "ethers"; 5 | import phunkAuctionFlywheel from '../abi/phunkAuctionFlywheel.json'; 6 | import { PhunksAuctionFlywheelService } from "./phunks.auction.flywheel.extension.service"; 7 | 8 | async function bootstrap() { 9 | const args = require('yargs') 10 | .option('contract', { string: true }) 11 | .option('tx', { string: true }) 12 | .argv; 13 | 14 | if (!args.block) { 15 | console.log('missing --block=[ethereum block number] parameter') 16 | return 17 | } 18 | if (!args.tx) { 19 | console.log('missing --tx=[ethereum tx hash] parameter') 20 | return 21 | } 22 | 23 | global.doNotStartAutomatically = true 24 | 25 | console.log('starting up') 26 | const app = await NestFactory.createApplicationContext(AppModule); 27 | 28 | const flyWheelService = app.get(PhunksAuctionFlywheelService); 29 | 30 | flyWheelService.startProvider() 31 | await delay(5000) 32 | 33 | const provider = flyWheelService.getWeb3Provider() 34 | 35 | const tokenContract = new ethers.Contract(flyWheelService.contractAddress, phunkAuctionFlywheel, provider); 36 | let filter = tokenContract.filters.PhunkSoldViaSignature(); 37 | const block = args.block 38 | 39 | if (!args.dryRun || args.dryRun !== 'true') { 40 | const events = (await tokenContract.queryFilter(filter, 41 | block, 42 | block)) 43 | //.filter(e => e.transactionHash === args.tx) 44 | 45 | for (let event of events) 46 | await flyWheelService.handleEvent(event) 47 | 48 | console.log('shuting down') 49 | 50 | await app.close(); 51 | console.log('end') 52 | exit(0) 53 | } 54 | } 55 | 56 | bootstrap(); 57 | 58 | function delay(ms: number) { 59 | return new Promise( resolve => setTimeout(resolve, ms) ); 60 | } -------------------------------------------------------------------------------- /src/extensions/phunks.auction.house.cli.extension.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { AppModule } from "../app.module"; 3 | import { exit } from "process"; 4 | import { ethers } from "ethers"; 5 | import phunkAuctionFlywheel from '../abi/phunkAuctionFlywheel.json'; 6 | import phunksAuctionHouse from '../abi/phunkAuctionHouse.json'; 7 | import { PhunksAuctionHouseService } from "./phunks.auction.house.extension.service"; 8 | 9 | async function bootstrap() { 10 | const args = require('yargs') 11 | .option('contract', { string: true }) 12 | .option('tx', { string: true }) 13 | .argv; 14 | 15 | if (!args.block) { 16 | console.log('missing --block=[ethereum block number] parameter') 17 | return 18 | } 19 | if (!args.tx) { 20 | console.log('missing --tx=[ethereum tx hash] parameter') 21 | return 22 | } 23 | 24 | global.doNotStartAutomatically = true 25 | 26 | console.log('starting up') 27 | const app = await NestFactory.createApplicationContext(AppModule); 28 | 29 | const auctionHouse = app.get(PhunksAuctionHouseService); 30 | 31 | auctionHouse.startProvider() 32 | await delay(5000) 33 | 34 | const provider = auctionHouse.getWeb3Provider() 35 | 36 | const tokenContract = new ethers.Contract(auctionHouse.contractAddress, phunksAuctionHouse, provider); 37 | let filter = tokenContract.filters.AuctionSettled(); 38 | const block = args.block 39 | 40 | if (!args.dryRun || args.dryRun !== 'true') { 41 | const events = (await tokenContract.queryFilter(filter, 42 | block, 43 | block)) 44 | //.filter(e => e.transactionHash === args.tx) 45 | 46 | for (let event of events) 47 | await auctionHouse.handleEvent(event) 48 | 49 | console.log('shuting down') 50 | 51 | await app.close(); 52 | console.log('end') 53 | await delay(5000) 54 | exit(0) 55 | } 56 | } 57 | 58 | bootstrap(); 59 | 60 | function delay(ms: number) { 61 | return new Promise( resolve => setTimeout(resolve, ms) ); 62 | } -------------------------------------------------------------------------------- /src/parsers/blur.io.sales.parser.ts: -------------------------------------------------------------------------------- 1 | import { AbiCoder, Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import blurABI from '../abi/blur.json'; 5 | 6 | const blurBiddingContractAddress = '0x0000000000a39bb272e79075ade125fd351887ac'; 7 | const blurInterface = new ethers.Interface(blurABI); 8 | const blurSalesContractAddressV2 = '0x39da41747a83aeE658334415666f3EF92DD0D541'; 9 | const blurSalesContractAddressV3 = '0xb2ecfe4e4d61f8790bbb9de2d1259b9e2410cea5'; 10 | 11 | export class BlurIOSalesParser implements LogParser { 12 | 13 | platform: string = 'blurio'; 14 | 15 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 16 | const result = logs 17 | .filter(l => l.address.toLowerCase() === blurBiddingContractAddress.toLowerCase()) 18 | .filter(l => { 19 | // find payment to blur 20 | const address = AbiCoder.defaultAbiCoder().decode(['address'], l?.topics[2])[0].toLowerCase() 21 | return address === blurSalesContractAddressV3.toLowerCase() || address === blurSalesContractAddressV2.toLowerCase() 22 | }) 23 | .map(l => { 24 | const relevantData = l.data.substring(2); 25 | const relevantDataSlice = relevantData.match(/.{1,64}/g); 26 | const amount = BigInt(`0x${relevantDataSlice[0]}`) 27 | 28 | return amount 29 | }) 30 | if (result.length) { 31 | const weiValue = result.reduce((previous,current) => previous + current, BigInt(0)); 32 | const count = logs 33 | .filter(l => l.address.toLowerCase() === config.contract_address.toLowerCase() && 34 | l.topics[0].toLowerCase() === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef').length 35 | const value = ethers.formatEther(weiValue/BigInt(count)); 36 | 37 | return parseFloat(value); 38 | } 39 | return undefined 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | 4 | import { Erc721SalesService } from './erc721sales.service'; 5 | // import { PhunksBidService } from './extensions/phunks.bid.extension.service'; 6 | // import { PhunksAuctionHouseService } from './extensions/phunks.auction.house.extension.service'; 7 | import { PhunksAuctionFlywheelService } from './extensions/phunks.auction.flywheel.extension.service'; 8 | import { StatisticsService } from './extensions/statistics.extension.service'; 9 | import { PhunksBidService } from './extensions/phunks.bid.extension.service'; 10 | import { PhunksAuctionHouseService } from './extensions/phunks.auction.house.extension.service'; 11 | import { PhunksErc721SpecialisedSalesService } from './extensions/phunks.erc721.specialised.service/phunks.erc721.specialised.service'; 12 | import { PhunksGifTwitterService } from './extensions/phunks.gif.twitter.extension.service'; 13 | import { DAOService } from './extensions/dao/dao.extension.service'; 14 | import { DAOController } from './extensions/dao/dao.controller'; 15 | import { ServeStaticModule } from '@nestjs/serve-static'; 16 | import { join } from 'path'; 17 | 18 | export const providers = [ 19 | Erc721SalesService, 20 | //PhunksErc721SpecialisedSalesService, 21 | //// 22 | // Below is a simple example of how to create and plug a custom 23 | // extension to the bot 24 | //// 25 | // 26 | // PhunksBidService, 27 | // PhunksAuctionHouseService, 28 | // PhunksAuctionFlywheelService, 29 | StatisticsService, 30 | DAOService, 31 | // PhunksGifTwitterService 32 | ] 33 | 34 | @Module({ 35 | imports: [ 36 | HttpModule, 37 | ServeStaticModule.forRoot({ 38 | rootPath: join(__dirname, '..', 'client'), 39 | })], 40 | providers: [ 41 | Erc721SalesService, 42 | StatisticsService, 43 | //// 44 | // Below is a simple example of how to create and plug a custom 45 | // extension to the bot 46 | //// 47 | // 48 | // PhunksBidService, 49 | // PhunksAuctionHouseService, 50 | // PhunksAuctionFlywheelService, 51 | // StatisticsService, 52 | // DAOService, 53 | ], 54 | controllers: [ 55 | DAOController 56 | ], 57 | 58 | }) 59 | 60 | export class AppModule { 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/clients/twitter.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../config'; 2 | import { BindTwitterRequestDto, BindTwitterResultDto } from 'src/extensions/dao/models'; 3 | import { EUploadMimeType, TwitterApi, UserV2Result } from 'twitter-api-v2'; 4 | 5 | export default class TwitterClient { 6 | 7 | client: TwitterApi; 8 | 9 | constructor() { 10 | this.client = process.env.hasOwnProperty('TWITTER_ACCESS_TOKEN_KEY') ? new TwitterApi({ 11 | accessToken: process.env.TWITTER_ACCESS_TOKEN_KEY, 12 | accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET, 13 | appKey: process.env.TWITTER_API_KEY, 14 | appSecret: process.env.TWITTER_API_KEY_SECRET, 15 | }) : undefined; 16 | } 17 | 18 | async startLogin() { 19 | const client = new TwitterApi({ 20 | clientId: process.env.TWITTER_CLIENT_ID, 21 | clientSecret: process.env.TWITTER_CLIENT_SECRET 22 | }) 23 | const result = client.generateOAuth2AuthLink(config.twitterAPIRedirectURL, { 24 | scope: ['tweet.read', 'users.read'] 25 | }) 26 | 27 | return result 28 | } 29 | 30 | async finalizeLogin(infos: any, request: BindTwitterRequestDto):Promise { 31 | const client = new TwitterApi({ 32 | clientId: process.env.TWITTER_CLIENT_ID, 33 | clientSecret: process.env.TWITTER_CLIENT_SECRET 34 | }) 35 | const { client: userClient, accessToken, refreshToken } = await client.loginWithOAuth2({ 36 | code: request.code, 37 | codeVerifier: infos.codeVerifier, 38 | redirectUri: `${config.twitterAPIRedirectURL}` 39 | }) 40 | console.log('client: ', userClient) 41 | const user = await userClient.currentUserV2() 42 | console.log('user', user) 43 | const currentUser = await userClient.v2.me({ 44 | "user.fields": "created_at" 45 | }) 46 | console.log('currentUser', currentUser) 47 | return { 48 | createdAt: currentUser.data.created_at, 49 | id: currentUser.data.id, 50 | name: currentUser.data.name, 51 | username: currentUser.data.username, 52 | accessToken, 53 | refreshToken 54 | } 55 | /* 56 | 57 | currentUser { 58 | data: { 59 | created_at: '2021-08-11T19:58:06.000Z', 60 | id: '1425547057486520329', 61 | name: 'tat2bu.eth', 62 | username: 'tat2bu' 63 | } 64 | }*/ 65 | } 66 | 67 | uploadMedia(processedImage: Buffer, options: { mimeType: EUploadMimeType; }): string | PromiseLike { 68 | this.client.appLogin 69 | return this.client.v1.uploadMedia(processedImage, options); 70 | } 71 | 72 | tweet(tweetText: string, options:any): any { 73 | return this.client.v2.tweet(tweetText, options) 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/extensions/phunks.auction.flywheel.extension.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { BaseService, TweetRequest } from '../base.service'; 4 | import { ethers } from 'ethers'; 5 | import phunkAuctionFlywheel from '../abi/phunkAuctionFlywheel.json'; 6 | import { config } from '../config'; 7 | import { createLogger } from 'src/logging.utils'; 8 | 9 | const logger = createLogger('phunksauction.service') 10 | 11 | @Injectable() 12 | export class PhunksAuctionFlywheelService extends BaseService { 13 | 14 | provider = this.getWeb3Provider(); 15 | contractAddress = '0x86b525ab8c5c9b8852f3a1bc79376335bcd2f962' 16 | 17 | constructor( 18 | protected readonly http: HttpService, 19 | ) { 20 | super(http) 21 | logger.info('creating PhunksAuctionFlywheelService') 22 | if (!global.doNotStartAutomatically) { 23 | this.startProvider() 24 | } 25 | } 26 | 27 | startProvider() { 28 | 29 | this.initDiscordClient() 30 | 31 | // Listen for auction settled event 32 | const tokenContract = new ethers.Contract(this.contractAddress, phunkAuctionFlywheel, this.provider); 33 | let filter = tokenContract.filters.PhunkSoldViaSignature(); 34 | tokenContract.on(filter, (async (event) => { 35 | await this.handleEvent(event) 36 | })) 37 | 38 | } 39 | 40 | async handleEvent(event:any) { 41 | const { phunkId, minSalePrice, seller, auctionId } = (event as any).args 42 | const imageUrl = `${config.local_auction_image_path}${phunkId.toString().padStart(4, '0')}.png`; 43 | const value = ethers.formatEther(minSalePrice) 44 | // If ens is configured, get ens addresses 45 | let ensTo: string; 46 | if (config.ens) { 47 | ensTo = await this.provider.lookupAddress(`${seller}`); 48 | } 49 | const block = await this.provider.getBlock(event.blockNumber) 50 | const transactionDate = block.date.toISOString() 51 | 52 | const request:TweetRequest = { 53 | logIndex: event.index, 54 | eventType: 'sale', 55 | platform: 'flywheel', 56 | transactionDate, 57 | initialFrom: seller, 58 | erc20Token: 'ethereum', 59 | from: this.shortenAddress('0x0e7f7d8007c0fccac2a813a25f205b9030697856'), 60 | tokenId: phunkId, 61 | to: ensTo ?? this.shortenAddress(seller), 62 | ether: parseFloat(value), 63 | transactionHash: event.transactionHash, 64 | alternateValue: 0, 65 | imageUrl 66 | } 67 | const tweet = await this.tweet(request, config.flywheelMessage); 68 | await this.discord(request, tweet.id, config.flywheelMessageDiscord, '#F99C1C', 'FLYWHEEL!'); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/parsers/blur.io.sweep.parser.ts: -------------------------------------------------------------------------------- 1 | import { AbiCoder, Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import blurABI from '../abi/blur.json'; 5 | 6 | const blurMarketplaceAddress = '0x39da41747a83aeE658334415666f3EF92DD0D541'; 7 | const blurBiddingContractAddress = '0x0000000000a39bb272e79075ade125fd351887ac'; 8 | const blurSalesContractAddressV2 = '0x39da41747a83aeE658334415666f3EF92DD0D541'; 9 | const blurSalesContractAddressV3 = '0xb2ecfe4e4d61f8790bbb9de2d1259b9e2410cea5'; 10 | 11 | export class BlurIOSweepParser implements LogParser { 12 | 13 | platform: string = 'blurio'; 14 | 15 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 16 | 17 | const result = (transaction.to.toLowerCase() != blurSalesContractAddressV3.toLowerCase() && 18 | transaction.to.toLowerCase() != blurSalesContractAddressV2.toLowerCase() && 19 | transaction.to.toLowerCase() != blurMarketplaceAddress.toLowerCase()) ? [] : 20 | logs.filter(l => l.address.toLowerCase() === blurSalesContractAddressV3.toLowerCase() || 21 | l.address.toLowerCase() === blurSalesContractAddressV2.toLowerCase()) 22 | 23 | if (result.length) { 24 | // if we're here, we weren't able to get the exact price, determinate it 25 | // using the overall price and the ether spent in tx 26 | // the only way to get an accurate result would be to run an EVM to track 27 | // internal txs 28 | const count = logs 29 | .filter(l => l.address.toLowerCase() === config.contract_address.toLowerCase() && 30 | l.topics[0].toLowerCase() === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef').length 31 | 32 | // look for blur.io custom ERC20 token if the ether amount is empty 33 | const { value } = transaction; 34 | let ether = ethers.formatEther(value.toString()); 35 | if (ether === '0.0') { 36 | const l = logs.filter(l => l.address.toLowerCase() === blurBiddingContractAddress.toLowerCase()) 37 | .reduce((previous, current) => { 38 | const relevantData = current.data.substring(2); 39 | const relevantDataSlice = relevantData.match(/.{1,64}/g); 40 | const value = BigInt(`0x${relevantDataSlice[0]}`); 41 | return previous + value 42 | }, BigInt(0)) 43 | 44 | ether = (parseFloat((l / BigInt('10000000000000000')).toString())/100).toString() 45 | } 46 | return parseFloat(ether)/count 47 | } 48 | return undefined 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/extensions/phunks.erc721.specialised.service/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/extensions/phunks.gif.twitter.extension.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { BaseService, TweetRequest } from '../base.service'; 4 | import needle from 'needle'; 5 | import { config } from '../config'; 6 | import { createLogger } from 'src/logging.utils'; 7 | 8 | const logger = createLogger('phunksauction.service') 9 | 10 | const url = `https://api.twitter.com/2/users/${config.gifModuleMentionnedUserId}/mentions`; 11 | 12 | @Injectable() 13 | export class PhunksGifTwitterService extends BaseService { 14 | 15 | 16 | constructor( 17 | protected readonly http: HttpService, 18 | ) { 19 | super(http) 20 | logger.info('creating PhunksGifTwitterService') 21 | this.getUserMentions() 22 | 23 | } 24 | 25 | 26 | // this is the ID for @TwitterDev 27 | async getUserMentions() { 28 | await new Promise( resolve => setTimeout(resolve, 1000) ); 29 | const t = this.twitterClient.client.v2.getActiveTokens() 30 | //const u = await this.twitterClient.client.v2. 31 | const r = await this.twitterClient.client.v2.userMentionTimeline(''+config.gifModuleMentionnedUserId) 32 | console.log(r) 33 | return 34 | let userMentions = []; 35 | let params = { 36 | "max_results": 100, 37 | "tweet.fields": "created_at" 38 | } 39 | 40 | const options = { 41 | headers: { 42 | "User-Agent": "v2UserMentionssJS", 43 | "authorization": `Bearer ${process.env.TWITTER_API_BEARER_TOKEN}` 44 | } 45 | } 46 | 47 | let hasNextPage = true; 48 | let nextToken = null; 49 | console.log("Retrieving mentions..."); 50 | while (hasNextPage) { 51 | let resp = await this.getPage(params, options, nextToken); 52 | if (resp && resp.meta && resp.meta.result_count && resp.meta.result_count > 0) { 53 | if (resp.data) { 54 | userMentions.push.apply(userMentions, resp.data); 55 | } 56 | if (resp.meta.next_token) { 57 | nextToken = resp.meta.next_token; 58 | } else { 59 | hasNextPage = false; 60 | } 61 | } else { 62 | hasNextPage = false; 63 | } 64 | } 65 | 66 | console.dir(userMentions, { 67 | depth: null 68 | }); 69 | 70 | console.log(`Got ${userMentions.length} mentions for user ID ${config.gifModuleMentionnedUserId}!`); 71 | 72 | } 73 | 74 | async getPage(params, options, nextToken) { 75 | if (nextToken) { 76 | params.pagination_token = nextToken; 77 | } 78 | 79 | try { 80 | const resp = await needle('get', url, params, options); 81 | 82 | if (resp.statusCode != 200) { 83 | console.log(`${resp.statusCode} ${resp.statusMessage}:\n${resp.body}`); 84 | return; 85 | } 86 | return resp.body; 87 | } catch (err) { 88 | throw new Error(`Request failed: ${err}`); 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/parsers/opensea.seaport.parser.ts: -------------------------------------------------------------------------------- 1 | import { AbiCoder, Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import { config } from "../config"; 4 | import openseaSeaportABI from '../abi/seaportABI.json'; 5 | import { createLogger } from "../logging.utils"; 6 | 7 | const seaportInterface = new ethers.Interface(openseaSeaportABI) 8 | const logger = createLogger('openseaseaport.parser') 9 | 10 | export class OpenSeaSeaportParser implements LogParser { 11 | 12 | platform: string = 'opensea'; 13 | 14 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 15 | 16 | 17 | let result = this.decode(transaction, logs, tokenId, false) 18 | if (result.length === 0) 19 | result = this.decode(transaction, logs, tokenId, true) 20 | if (result.length) return parseFloat(result.reduce((previous,current) => previous + current, BigInt(0)).toString())/1000; 21 | return undefined 22 | } 23 | 24 | decode(transaction:TransactionResponse, logs: Log[], tokenId: string, allowUnknownOfferer:boolean) { 25 | return logs.map((log: any) => { 26 | if (log.topics[0].toLowerCase() === '0x9d9af8e38d66c62e2c12f0225249fd9d721c54b83f48d9352c97c6cacdcb6f31') { 27 | 28 | const logDescription = seaportInterface.parseLog(log); 29 | 30 | if (logDescription.args.offer.filter( o => o.identifier.toString() !== '0').length && 31 | logDescription.args.consideration.filter( o => o.identifier.toString() !== '0').length) { 32 | // complex opensea trade detected, ignore 33 | logger.info(`complex opensea trade detected for ${transaction.hash} log ${log.index}, ignoring...`) 34 | return 35 | } 36 | 37 | if (!allowUnknownOfferer && transaction.from !== logDescription.args.offerer) { 38 | logger.info(`offerer is not the transaction emitter, ${transaction.hash} log ${log.index}, ignoring...`) 39 | return 40 | } 41 | 42 | const matchingOffers = logDescription.args.offer.filter( 43 | o => o.identifier.toString() === tokenId || 44 | o.identifier.toString() === '0'); 45 | 46 | const tokenCount = logDescription.args.offer.length; 47 | 48 | if (matchingOffers.length === 0) { 49 | return 50 | } 51 | let amounts = logDescription.args.consideration.map(c => BigInt(c.amount)) 52 | // add weth 53 | const wethOffers = matchingOffers.map(o => o.token === '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' && o.amount > 0 ? BigInt(o.amount) : BigInt(0)); 54 | if (wethOffers.length > 0 && wethOffers[0] != BigInt(0)) { 55 | amounts = wethOffers 56 | } 57 | const amount = amounts.reduce((previous,current) => previous + current, BigInt(0)) 58 | return amount / BigInt('1000000000000000') / BigInt(tokenCount) 59 | } 60 | }).filter(n => n !== undefined) 61 | } 62 | } -------------------------------------------------------------------------------- /src/clients/discord.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import { config } from '../config'; 3 | import { Client, MessageAttachment, MessageEmbed, TextChannel } from "discord.js"; 4 | import { Routes } from "discord-api-types/v10"; 5 | import { REST } from "@discordjs/rest"; 6 | 7 | const discordCommands = [] 8 | let inited = false 9 | const callbacks: Function[] = [] 10 | const interactionsListener:any[] = []; 11 | let client: Client; 12 | const channels: TextChannel[] = []; 13 | 14 | export default class DiscordClient { 15 | 16 | getDiscordCommands() { 17 | return discordCommands 18 | } 19 | 20 | setup: boolean; 21 | 22 | getClient():Client { 23 | return client 24 | } 25 | 26 | getInteractionsListener() { 27 | return interactionsListener 28 | } 29 | 30 | init(callback:Function=undefined) { 31 | if (!process.env.DISCORD_TOKEN) return; 32 | if (!client) { 33 | console.log(`new discord client`) 34 | client = new Client({ intents: ['GUILD_MESSAGE_REACTIONS', 'GUILD_MEMBERS', 'MESSAGE_CONTENT'] }); 35 | client.once('ready', async (c) => { 36 | 37 | console.log('logged in', c.user.username) 38 | const configurationChannels = config.discord_channels.split(','); 39 | for (let channel of configurationChannels) { 40 | //console.log(`fetching ${channel}`) 41 | channels.push( 42 | (await client.channels.fetch(channel)) as TextChannel, 43 | ); 44 | } 45 | const rest = new REST().setToken(process.env.DISCORD_TOKEN); 46 | 47 | const guildIds = config.discord_guild_ids.split(',') 48 | 49 | if (callback) callback() 50 | if (callbacks.length) callbacks.forEach(c => c()) 51 | 52 | client.on('interactionCreate', (interaction) => { 53 | for (const listener of interactionsListener) { 54 | listener(interaction) 55 | } 56 | }) 57 | guildIds.forEach(async (guildId) => { 58 | await rest.put( 59 | Routes.applicationGuildCommands(config.discord_client_id, guildId), 60 | { body: discordCommands }, 61 | ); 62 | }) 63 | }); 64 | } 65 | if (!inited) { 66 | inited = true 67 | client.login(process.env.DISCORD_TOKEN); 68 | } else if (client.isReady()) { 69 | if (callback !== undefined) callback() 70 | } else { 71 | if (callback !== undefined) callbacks.push(callback) 72 | } 73 | this.setup = true; 74 | } 75 | 76 | 77 | async sendEmbed(embed:MessageEmbed, image:string|Buffer, platform:string) { 78 | channels.forEach(async (channel) => { 79 | await channel.send({ 80 | embeds: [embed], 81 | files: [ 82 | { attachment: image, name: 'token.png' }, 83 | { attachment: platform, name: 'platform.png' }, 84 | ], 85 | }); 86 | }); 87 | } 88 | 89 | async send(text: string, images: string[]) { 90 | channels.forEach(async (channel) => { 91 | await channel.send({ 92 | content: text, 93 | files: images, 94 | }); 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /client/twitter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 21 | 22 | 23 | 61 | 62 | 63 |

Cryptophunk Discord / Twitter account binder

64 | 65 |
Connecting...
66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/cli.module.ts: -------------------------------------------------------------------------------- 1 | global.doNotStartAutomatically = true 2 | import { promises as fs } from 'fs'; 3 | import { NestFactory } from "@nestjs/core"; 4 | import { AppModule } from "./app.module"; 5 | import { Erc721SalesService } from "./erc721sales.service"; 6 | import { exit } from "process"; 7 | import { config } from "./config"; 8 | import { ethers } from "ethers"; 9 | import erc721abi from './abi/erc721.json' 10 | import { StatisticsService } from "./extensions/statistics.extension.service"; 11 | 12 | async function bootstrap() { 13 | const args = require('yargs') 14 | .option('action', { string: true }) 15 | .option('contract', { string: true }) 16 | .option('tx', { string: true }) 17 | .argv; 18 | 19 | if (!args.action) { 20 | console.log('missing --action=[index or tweet] parameter') 21 | exit(1) 22 | return 23 | } 24 | if (!args.block && args.action !== 'extract') { 25 | console.log('missing --block=[ethereum block number] parameter') 26 | exit(1) 27 | return 28 | } 29 | if (!args.tx && args.action !== 'extract') { 30 | console.log('missing --tx=[ethereum tx hash] parameter') 31 | exit(1) 32 | return 33 | } 34 | 35 | console.log('starting up') 36 | const app = await NestFactory.createApplicationContext(AppModule); 37 | 38 | const saleService = app.get(Erc721SalesService); 39 | const statService = app.get(StatisticsService); 40 | 41 | // saleService.startProvider() 42 | await delay(5000) 43 | 44 | const provider = saleService.getWeb3Provider() 45 | 46 | if (args.contract) 47 | config.contract_address = args.contract 48 | 49 | const tokenContract = new ethers.Contract(config.contract_address, erc721abi, provider); 50 | let filter = tokenContract.filters.Transfer(); 51 | const block = args.block 52 | 53 | if (!args.dryRun || args.dryRun !== 'true') { 54 | 55 | if (args.action === 'extract') { 56 | for (let i=0; i<10000; i++) { 57 | const tokenId = i.toString().padStart(4, '0') 58 | await saleService.getTokenMetadata(tokenId, false) 59 | } 60 | return 61 | } 62 | 63 | const events = (await tokenContract.queryFilter(filter, 64 | block, 65 | block)) 66 | .filter(e => e.transactionHash === args.tx) 67 | 68 | if (args.action === 'tweet') { 69 | const results = await Promise.all( 70 | events.map(async (e) => await saleService.getTransactionDetails(e)) 71 | ) 72 | 73 | let logs = '' 74 | results.filter(r => r !== undefined).forEach(r => { 75 | logs += `${r.tokenId} sold for ${r.alternateValue}\n` 76 | }) 77 | console.log(logs) 78 | for (let r of results) { 79 | await saleService.dispatch(r) 80 | } 81 | } else { 82 | statService.prepareStatements() 83 | await statService.handleEvents(events) 84 | } 85 | } else { 86 | console.log('not dispatching event ') 87 | } 88 | 89 | console.log('shuting down') 90 | 91 | await app.close(); 92 | console.log('end') 93 | exit(0) 94 | } 95 | 96 | bootstrap(); 97 | 98 | 99 | function delay(ms: number) { 100 | return new Promise( resolve => setTimeout(resolve, ms) ); 101 | } -------------------------------------------------------------------------------- /src/parsers/nftx.parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, TransactionResponse, ethers } from "ethers"; 2 | import { LogParser } from "./parser.definition"; 3 | import nftxABI from '../abi/nftxABI.json'; 4 | import { config } from "../config"; 5 | 6 | const nftxInterface = new ethers.Interface(nftxABI); 7 | 8 | export class NFTXParser implements LogParser { 9 | 10 | platform: string = 'nftx'; 11 | 12 | parseLogs(transaction:TransactionResponse, logs: Log[], tokenId: string): number { 13 | const result = logs.map((log: any) => { 14 | 15 | // direct buy from vault 16 | if (log.topics[0].toLowerCase() === '0x1cdb5ee3c47e1a706ac452b89698e5e3f2ff4f835ca72dde8936d0f4fcf37d81') { 17 | const relevantData = log.data.substring(2); 18 | const relevantDataSlice = relevantData.match(/.{1,64}/g); 19 | return BigInt(`0x${relevantDataSlice[1]}`) / BigInt('1000000000000000'); 20 | } else if (log.topics[0].toLowerCase() === '0x63b13f6307f284441e029836b0c22eb91eb62a7ad555670061157930ce884f4e') { 21 | const parsedLog = nftxInterface.parseLog(log) 22 | 23 | // check that the current transfer is NFTX related 24 | if (!parsedLog.args.nftIds.filter(n => BigInt(n).toString() === tokenId).length) { 25 | return 26 | } 27 | 28 | // redeem, find corresponding token bought 29 | const swaps = logs.filter((log2: any) => log2.topics[0].toLowerCase() === '0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822') 30 | .map(b => { 31 | const relevantData = b.data.substring(2); 32 | const relevantDataSlice = relevantData.match(/.{1,64}/g); 33 | const moneyIn = BigInt(`0x${relevantDataSlice[0]}`) 34 | if (moneyIn > BigInt(0)) 35 | return moneyIn / BigInt('1000000000000000'); 36 | else { 37 | const moneyIn2 = BigInt(`0x${relevantDataSlice[1]}`) 38 | return moneyIn2 / BigInt('1000000000000000'); 39 | } 40 | }) 41 | if (swaps.length) return swaps.reduce((previous, current) => previous + current, BigInt(0)) 42 | } 43 | }).filter(n => n !== undefined) 44 | 45 | if (result.length) { 46 | // find the number of token transferred to adjust amount per token 47 | const redeemLog = logs.filter((log: any) => log.topics[0].toLowerCase() === '0x63b13f6307f284441e029836b0c22eb91eb62a7ad555670061157930ce884f4e')[0] as any 48 | let tokenCount = 1 49 | if (redeemLog) { 50 | const parsedLog = nftxInterface.parseLog(redeemLog) 51 | tokenCount = Math.max(parsedLog.args.nftIds.length, 1) 52 | } else { 53 | // count the number of tokens transfered 54 | tokenCount = logs 55 | .filter(l => l.address.toLowerCase() === config.contract_address.toLowerCase() && 56 | l.topics[0].toLowerCase() === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') 57 | .map(l => l.topics[3]) 58 | // take unique value 59 | .filter((value, index, array) => array.indexOf(value) === index) 60 | .length 61 | } 62 | return parseFloat(result[0].toString())/tokenCount/1000; 63 | } 64 | return undefined 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /client/polls/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 32 | 33 | 43 | 78 | 79 | 80 | 81 | 82 |

Cryptophunk Open Polls

83 | 84 |
Loading...
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/extensions/phunks.bid.extension.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { BaseService, TweetRequest } from '../base.service'; 4 | import { ethers } from 'ethers'; 5 | import notLarvaLabsAbi from '../abi/notlarvalabs.json'; 6 | import { config } from '../config'; 7 | import { createLogger } from 'src/logging.utils'; 8 | 9 | const logger = createLogger('phunks.bid.extension.service') 10 | 11 | @Injectable() 12 | export class PhunksBidService extends BaseService { 13 | 14 | provider = this.getWeb3Provider(); 15 | 16 | constructor( 17 | protected readonly http: HttpService, 18 | ) { 19 | super(http) 20 | logger.info('creating PhunksBidService') 21 | 22 | this.initDiscordClient() 23 | 24 | // Listen for Bid event 25 | const tokenContract = new ethers.Contract('0xd6c037bE7FA60587e174db7A6710f7635d2971e7', notLarvaLabsAbi, this.provider); 26 | let filter = tokenContract.filters.PhunkBidEntered(); 27 | tokenContract.on(filter, (async (event) => { 28 | const token = event.args.phunkIndex 29 | const amount = event.args.value 30 | const from = event?.args[2]; 31 | 32 | const imageUrl = `${config.local_bids_image_path}${token}.png`; 33 | const value = ethers.formatEther(amount) 34 | // If ens is configured, get ens addresses 35 | let ensFrom: string; 36 | if (config.ens) { 37 | ensFrom = await this.provider.lookupAddress(`${from}`); 38 | } 39 | const block = await this.provider.getBlock(event.blockNumber) 40 | const transactionDate = block.date.toISOString() 41 | 42 | const request:TweetRequest = { 43 | logIndex: event.index, 44 | eventType: 'bid', 45 | platform: 'notlarvalabs', 46 | initialFrom: from, 47 | transactionDate, 48 | erc20Token: 'ethereum', 49 | from: ensFrom ?? this.shortenAddress(from), 50 | tokenId: token, 51 | ether: parseFloat(value), 52 | transactionHash: event.transactionHash, 53 | alternateValue: 0, 54 | imageUrl 55 | } 56 | const tweet = await this.tweet(request, config.bidMessage); 57 | await this.discord(request, tweet.id, config.bidMessageDiscord, '#9856B7', 'BID!'); 58 | })) 59 | 60 | // uncomment this to test the plugin 61 | return 62 | tokenContract.queryFilter(filter, 63 | 17987050, 64 | 17987050).then(async (events:any) => { 65 | for (const event of events) { 66 | if (event?.args.length < 3) return 67 | const from = event?.args[2]; 68 | // If ens is configured, get ens addresses 69 | let ensFrom: string; 70 | if (config.ens) { 71 | ensFrom = await this.provider.lookupAddress(`${from}`); 72 | } 73 | const value = ethers.formatEther(event.args.value); 74 | const imageUrl = `${config.local_bids_image_path}${event.args.phunkIndex}.png`; 75 | const block = await this.provider.getBlock(event.blockNumber) 76 | const transactionDate = block.date.toISOString() 77 | 78 | const request:TweetRequest = { 79 | logIndex: event.index, 80 | eventType: 'bid', 81 | platform: 'notlarvalabs', 82 | initialFrom: from, 83 | erc20Token: 'ethereum', 84 | transactionDate, 85 | from: ensFrom ?? this.shortenAddress(from), 86 | tokenId: event.args.phunkIndex, 87 | ether: parseFloat(value), 88 | transactionHash: event.transactionHash, 89 | alternateValue: 0, 90 | imageUrl 91 | } 92 | const tweet = await this.tweet(request, config.bidMessage); 93 | await this.discord(request, tweet.id, config.bidMessageDiscord, '#8119B7', 'BID!'); 94 | } 95 | }); 96 | 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/extensions/phunks.erc721.specialised.service/images/logo-phunk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-sales-twitter-bot", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "chopper", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "cli": "nest start --entryFile cli.module.js --", 11 | "cli:extension:punk": "nest start --entryFile extensions/cryptopunks.extensions/cryptopunks.extension.service.cli.js --", 12 | "cli:extension:punk:erc721": "nest start --entryFile extensions/cryptopunks.extensions/cryptopunks.erc721.specialised.service.cli.js --", 13 | "cli:extension:punk:bid": "nest start --entryFile extensions/cryptopunks.extensions/cryptopunks.bid.extension.service.cli.js --", 14 | "cli:extension:flywheel": "nest start --entryFile extensions/phunks.auction.flywheel.cli.extension.js --", 15 | "cli:extension:auctionhouse": "nest start --entryFile extensions/phunks.auction.house.cli.extension.js --", 16 | "start": "nest start", 17 | "start:dev": "nest start --watch", 18 | "start:debug": "nest start --debug --watch", 19 | "start:prod": "node dist/main", 20 | "start:prod-with-watchdog": "concurrently npm:start:prod --restart-tries -1 --restart-after 5000", 21 | "test": "jest --runInBand --detectOpenHandles --forceExit && jest-coverage-badges", 22 | "test:watch": "jest --watch", 23 | "copy:assets": "cpx 'src/extensions/cryptopunks.extensions/fonts/**' 'dist/extensions/cryptopunks.extensions/fonts/' && cpx 'src/extensions/cryptopunks.extensions/images/**' 'dist/extensions/cryptopunks.extensions/images/'" 24 | }, 25 | "dependencies": { 26 | "@discordjs/rest": "^2.0.0", 27 | "@nestjs/axios": "^3.0.0", 28 | "@nestjs/common": "^10", 29 | "@nestjs/core": "^10", 30 | "@nestjs/platform-express": "^10", 31 | "@nestjs/serve-static": "^4.0.0", 32 | "alchemy-sdk": "^2.10.0", 33 | "better-sqlite3": "^8.5.0", 34 | "canvas": "^2.11.2", 35 | "chart.js": "^3.9.1", 36 | "chartjs-node-canvas": "^4.1.6", 37 | "concurrently": "^7.2.2", 38 | "cpx": "^1.5.0", 39 | "currency.js": "^2.0.4", 40 | "date-fns": "^2.30.0", 41 | "date-fns-tz": "^2.0.0", 42 | "date-utils": "^1.2.21", 43 | "discord-api-types": "^0.37.53", 44 | "discord.js": "^13", 45 | "dotenv": "^16.0.0", 46 | "ethers": "^6.7.0", 47 | "express": "^4.18.2", 48 | "jest-coverage-badges": "^1.1.2", 49 | "lodash": "^4.17.21", 50 | "moment": "^2.29.1", 51 | "needle": "^3.2.0", 52 | "node-fetch": "^2.7.0", 53 | "readline-sync": "^1.4.10", 54 | "reflect-metadata": "^0.1.13", 55 | "rimraf": "^3.0.2", 56 | "rxjs": "^7.2.0", 57 | "service": "^0.1.4", 58 | "svg2img": "^1.0.0-beta.2", 59 | "twitter-api-v2": "^1.15.0", 60 | "web3-utils": "^1.7.1", 61 | "winston": "^3.10.0", 62 | "winston-daily-rotate-file": "^4.7.1", 63 | "yargs": "^17.7.2" 64 | }, 65 | "devDependencies": { 66 | "@nestjs/cli": "^10.0.0", 67 | "@nestjs/schematics": "^10.0.0", 68 | "@nestjs/testing": "^10.0.0", 69 | "@types/express": "^4.17.13", 70 | "@types/jest": "27.4.1", 71 | "@types/node": "^16.0.0", 72 | "@types/supertest": "^2.0.11", 73 | "@typescript-eslint/eslint-plugin": "^5.0.0", 74 | "@typescript-eslint/parser": "^5.0.0", 75 | "eslint": "^8.0.1", 76 | "eslint-config-prettier": "^8.3.0", 77 | "eslint-plugin-prettier": "^4.0.0", 78 | "jest": "^29.7.0", 79 | "madge": "^6.1.0", 80 | "prettier": "^2.3.2", 81 | "source-map-support": "^0.5.20", 82 | "supertest": "^6.1.3", 83 | "ts-jest": "^29.1.1", 84 | "ts-loader": "^9.2.3", 85 | "ts-node": "^10.0.0", 86 | "tsconfig-paths": "^3.10.1", 87 | "typescript": "^4.3.5" 88 | }, 89 | "jest": { 90 | "globals": { 91 | "doNotStartAutomatically": true, 92 | "noWatchdog": true, 93 | "providerForceHTTPS": true 94 | }, 95 | "moduleFileExtensions": [ 96 | "js", 97 | "json", 98 | "ts" 99 | ], 100 | "rootDir": "src", 101 | "testRegex": ".*\\.spec\\.ts$", 102 | "transform": { 103 | "^.+\\.(t|j)s$": "ts-jest" 104 | }, 105 | "collectCoverageFrom": [ 106 | "**/*.(t|j)s" 107 | ], 108 | "coveragePathIgnorePatterns": [ 109 | "src/extensions" 110 | ], 111 | "coverageReporters": [ 112 | "json-summary", 113 | "html", 114 | "cobertura" 115 | ], 116 | "collectCoverage": true, 117 | "coverageDirectory": "../coverage", 118 | "testEnvironment": "node" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/extensions/phunks.auction.house.extension.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { promises as fs } from 'fs'; 3 | import { HttpService } from '@nestjs/axios'; 4 | import { BaseService, TweetRequest } from '../base.service'; 5 | import { ethers } from 'ethers'; 6 | import phunksAuctionHouse from '../abi/phunkAuctionHouse.json'; 7 | import { config } from '../config'; 8 | import { createLogger } from 'src/logging.utils'; 9 | 10 | const logger = createLogger('phunksauctionhouse.service') 11 | 12 | @Injectable() 13 | export class PhunksAuctionHouseService extends BaseService { 14 | 15 | provider = this.getWeb3Provider(); 16 | currentBlock:number = -1 17 | contractAddress = '0x0e7f7d8007c0fccac2a813a25f205b9030697856' 18 | 19 | constructor( 20 | protected readonly http: HttpService, 21 | ) { 22 | super(http) 23 | logger.info('creating PhunksAuctionHouseService') 24 | if (!global.doNotStartAutomatically) { 25 | this.startProvider() 26 | } 27 | } 28 | 29 | async startProvider() { 30 | 31 | const CHUNK_SIZE = 20 32 | this.initDiscordClient() 33 | 34 | // Listen for auction settled event 35 | const tokenContract = new ethers.Contract(this.contractAddress, phunksAuctionHouse, this.provider); 36 | let filter = tokenContract.filters.AuctionSettled(); 37 | 38 | try { 39 | this.currentBlock = parseInt(await fs.readFile(this.getPositionFile(), { encoding: 'utf8' })) 40 | } catch (err) { 41 | } 42 | if (isNaN(this.currentBlock) || this.currentBlock <= 0) { 43 | this.currentBlock = await this.getWeb3Provider().getBlockNumber() 44 | await this.updatePosition(this.currentBlock) 45 | } 46 | console.log(`position: ${this.currentBlock}`) 47 | let retryCount = 0 48 | let latestTweetedBlock = 0 49 | let latestTweetedTx = '' 50 | 51 | while (true) { 52 | try { 53 | const latestAvailableBlock = await this.provider.getBlockNumber() 54 | if (this.currentBlock >= latestAvailableBlock) { 55 | logger.info(`latest block reached (${latestAvailableBlock}), waiting the next available block...`) 56 | await delay(10000) 57 | continue 58 | } 59 | 60 | console.log(`checking (phunk auction module) ${this.currentBlock}`) 61 | const events = await tokenContract.queryFilter(filter, this.currentBlock, this.currentBlock + CHUNK_SIZE) 62 | 63 | for (let event of events) { 64 | latestTweetedBlock = event.blockNumber 65 | latestTweetedTx = event.transactionHash 66 | await this.handleEvent(event) 67 | } 68 | 69 | this.currentBlock += CHUNK_SIZE 70 | if (this.currentBlock > latestAvailableBlock) this.currentBlock = latestAvailableBlock + 1 71 | await this.updatePosition(latestAvailableBlock) 72 | } catch (err) { 73 | console.log(err) 74 | retryCount++ 75 | if (retryCount > 5) { 76 | console.log(`stop retrying, failing on ${latestTweetedTx}, moving to next block`) 77 | this.currentBlock = latestTweetedBlock + 1 78 | retryCount = 0 79 | } 80 | } 81 | } 82 | 83 | } 84 | 85 | async handleEvent(event) { 86 | const { phunkId, winner, amount, auctionId } =  ( event as any).args 87 | const imageUrl = `${config.local_auction_image_path}${phunkId.toString().padStart(4, '0')}.png`; 88 | const value = ethers.formatEther(amount) 89 | // If ens is configured, get ens addresses 90 | let ensTo: string; 91 | if (config.ens) { 92 | ensTo = await this.provider.lookupAddress(`${winner}`); 93 | } 94 | const block = await this.provider.getBlock(event.blockNumber) 95 | const transactionDate = block.date.toISOString() 96 | 97 | const request:TweetRequest = { 98 | logIndex: event.index, 99 | eventType: 'sale', 100 | platform: 'auctionhouse', 101 | transactionDate, 102 | erc20Token: 'ethereum', 103 | initialFrom: '0x0e7f7d8007c0fccac2a813a25f205b9030697856', 104 | initialTo: winner, 105 | from: this.shortenAddress('0x0e7f7d8007c0fccac2a813a25f205b9030697856'), 106 | tokenId: phunkId, 107 | to: ensTo ?? this.shortenAddress(winner), 108 | ether: parseFloat(value), 109 | transactionHash: event.transactionHash, 110 | additionalText: `https://phunks.auction/auction/${auctionId}`, 111 | alternateValue: 0, 112 | imageUrl 113 | } 114 | const tweet = await this.tweet(request, config.auctionMessage); 115 | await this.discord(request, tweet.id, config.auctionMessageDiscord, '#FF04B4', 'AUCTION!'); 116 | } 117 | 118 | getPositionFile() { 119 | return 'phunks.auction.house.position.txt' 120 | } 121 | 122 | } 123 | 124 | function delay(ms: number) { 125 | return new Promise( resolve => setTimeout(resolve, ms) ); 126 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 21 | 22 | 23 | 95 | 96 | 97 |

Cryptophunk Discord / Web3 wallet binder

98 | 99 | connect discord 100 | 101 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Discord and Twitter NFT sales bot

2 | 3 | [![coverage](https://crypto-phunks.github.io/nft-sales-twitter-bot/badge-lines.svg?update2)](https://crypto-phunks.github.io/nft-sales-twitter-bot/) 4 | 5 | ## Description 6 | 7 | Tweets real-time NFT sales for ERC721 Smart contracts 8 | 9 | In order to use this you’ll need to apply for Elevated access via the Twitter Developer Portal. You can learn more [here](https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve). 10 | 11 | ## Installation 12 | 13 | ```bash 14 | $ npm install 15 | ``` 16 | 17 | 1. Create `.env` file & add contents from `example.env` -- Add your API credentials. 18 | 2. Edit the `src/config.ts` file to add your smart contract & customize the tweet parameters. 19 | 3. Edit `src/erc721sales.service.ts` to customize for your use (Experienced users only & not a requirement). 20 | 4. Build & Deploy `npm run build` 21 | 5. Feel free to reach out on twitter 22 | 23 | ## Use local images 24 | 25 | If you want to improve performances, you may want to use local images, to do so, simply 26 | set the following variables in the configuration: 27 | 28 | ``` 29 | use_local_images: true, 30 | local_image_path: './token_images/tokens', 31 | ``` 32 | 33 | The `local_image_path` will be suffixed with the token number, ie, here, it will seek for an image 34 | named `./token_images/tokens0034.png` if the token #34 is sold. 35 | 36 | ## Plugins / extendability 37 | 38 | You can create custom interactions by implementing custom extensions by extending the `BaseService` base 39 | class, an example is provided in the `extensions/phunks.bid.extension.service.ts`. Once implemented, you can activate an extension by importing it in the `AppModule` service providers, ie: 40 | 41 | ``` 42 | @Module({ 43 | imports: [HttpModule], 44 | controllers: [], 45 | providers: [ 46 | Erc721SalesService, 47 | PhunksBidService, 48 | ], 49 | }) 50 | ``` 51 | 52 | ## Discord support 53 | 54 | Just add a `DISCORD_TOKEN` in your `.env` file, also add `saleMessageDiscord` and `discord_channel` keys. The later must contain the identifier of the channel you want the bot to post the sale events in. You'll also need the `local_image_path` containing tokens images. 55 | 56 | You can add a link to the tweet that's been generated in the discord message using `` in your `saleMessageDiscord` template. 57 | 58 | To setup the bot, lead to https://discord.com/developers and create an application and a bot, then invite the bot you just created using the following link: https://discord.com/api/oauth2/authorize?client_id=[yourDiscordAppclientId]&permissions=268512336&scope=bot%20applications.commands, then ensure that the invited bot is allowed to access the channel ID you want your bot to post into. 59 | 60 | ## Statistics module 61 | 62 | You can enable the optional statistics module in the `AppModule` definition file `app.module.ts`. When 63 | enabled, it will index the blockchain for the specified contract in the configuration, starting at 64 | block `statistic_initial_block`. Once indexed, the discord bot will reply to the following commands: 65 | 66 | - `/owned ` will display a list of the owned tokens by a wallet. 67 | - `/wallet ` will display some statistics for a given wallet (owned token, days since the first owned tokens, total volume of transaction) 68 | - `/volume ` will display a volume of transactions per marketplace. 69 | - `/graph ` will display a graph showing the average price and the volume of transaction over time, the `wallet` parameter is optional and will filter out the data on the specified wallet. 70 | - `/traders ` will display the top 20 traders for the tracked collection over the specified time window, the `wallet` parameter is optional and will force the given wallet to be displayed along it's rank if it has traded at least one NFT over the specified period. 71 | - `/index ` force indexation by the statistic module of the given transaction within the given block. 72 | - `/transaction ` displays indexed informations about a given transaction, usefull for debugging purpose. 73 | 74 | The message templates for each command can be customized through the configuration file. 75 | 76 | ## DAO module 77 | 78 | You can enable the optional DAO module in the `AppModule` definition file `app.module.ts`, it requires 79 | the statistics module to be enabled as well. When enabled, it will make available commands to let users 80 | bind their web3 wallets to their discord account through the `/bind` command ; creating a bridge between 81 | web3 ownership tracability and discord community management features. 82 | 83 | This binding enable a lot of possibilities: 84 | 85 | - The bounded wallets can be used to grant specific roles on the configured discord server, check out the 86 | [sample configuration file](https://github.com/Crypto-Phunks/nft-sales-twitter-bot/blob/main/src/config.ts) to see 87 | how to grant roles for users owning a token of the collection, for the ones who originally minted the collection, 88 | or the ones owning tokens with a specific trait. 89 | 90 | - The DAO module includes a voting mechanism that let the discord administrators to create polls for the communities, 91 | these votes are anonymous and administrable through the `/createpoll `, `/pollresults `, 92 | and `/listpolls` commands. 93 | 94 | 95 | ## CLI mode 96 | 97 | You can use this app as a standalone cli using the following command line along with the `block` and `tx` parameter. The optional `contract` parameter can be used to override the contract from the configuration. ie: 98 | 99 | ``` 100 | npm run cli -- --action=tweet --contract=0xA6Cd272874Ee7C872Eb66801Eff62784C0b13285 --block=17886451 --tx=0x6018d9290709e7d34c820b23820aaacf960af9c4f073b661136d49fc0994d6c9 101 | ``` 102 | 103 | Will replay the given transaction and trigger the tweets detected within it. And: 104 | 105 | ``` 106 | npm run cli -- --action=index --contract=0xf07468eAd8cf26c752C676E43C814FEe9c8CF402 --block=14763639 --tx=0x1e0667e75e4aba0f5cb303d79969a7514ed1248b91889d2bb553863992ddff7e 107 | ``` 108 | 109 | Will index the given transaction into the statistics module. 110 | 111 | ## Running the app 112 | 113 | ```bash 114 | # development 115 | $ npm run start 116 | 117 | # watch mode 118 | $ npm run start:dev 119 | 120 | # production mode 121 | $ npm run start:prod 122 | ``` 123 | 124 | ## Created by 125 | 126 | The phunk community to serve the NFT space. 127 | 128 | ## License 129 | 130 | Created using [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 131 | Nest is [MIT licensed](LICENSE). 132 | -------------------------------------------------------------------------------- /client/fonts/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Transfonter demo 10 | 11 | 158 | 159 | 160 |
161 |
162 |

Retro Computer

163 |
.your-style {
164 |     font-family: 'Retro Computer';
165 |     font-weight: normal;
166 |     font-style: normal;
167 | }
168 |
169 | <link rel="preload" href="RetroComputer.woff2" as="font" type="font/woff2" crossorigin>
170 |
171 |

172 | abcdefghijklmnopqrstuvwxyz
173 | ABCDEFGHIJKLMNOPQRSTUVWXYZ
174 | 0123456789.:,;()*!?'@#<>$%&^+-=~ 175 |

176 |

The quick brown fox jumps over the lazy dog.

177 |

The quick brown fox jumps over the lazy dog.

178 |

The quick brown fox jumps over the lazy dog.

179 |

The quick brown fox jumps over the lazy dog.

180 |

The quick brown fox jumps over the lazy dog.

181 |

The quick brown fox jumps over the lazy dog.

182 |

The quick brown fox jumps over the lazy dog.

183 |

The quick brown fox jumps over the lazy dog.

184 |

The quick brown fox jumps over the lazy dog.

185 |

The quick brown fox jumps over the lazy dog.

186 |

The quick brown fox jumps over the lazy dog.

187 |
188 |
189 | 190 |
191 | 192 | 193 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /src/extensions/phunks.erc721.specialised.service/phunks.erc721.specialised.service.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import erc721abi from '../../abi/erc721.json' 3 | 4 | import { readFile } from 'fs/promises'; 5 | import punkAttributes from './json/punkAttributes.json' 6 | import { Injectable } from '@nestjs/common'; 7 | import { HttpService } from '@nestjs/axios'; 8 | import dotenv from 'dotenv'; 9 | dotenv.config(); 10 | 11 | import { createLogger } from '../../logging.utils'; 12 | import { Erc721SalesService } from 'src/erc721sales.service'; 13 | import { createCanvas, loadImage, registerFont } from 'canvas'; 14 | import { ethers } from 'ethers'; 15 | import { config } from '../../config'; 16 | import { TweetRequest } from 'src/base.service'; 17 | import path from 'path'; 18 | import svg2img from 'svg2img'; 19 | 20 | const logger = createLogger('phunks.erc721sales.service') 21 | 22 | @Injectable() 23 | export class PhunksErc721SpecialisedSalesService extends Erc721SalesService { 24 | 25 | 26 | constructor( 27 | protected readonly http: HttpService, 28 | ) { 29 | super(http) 30 | 31 | if (!global.doNotStartAutomatically) { 32 | this.startProvider() 33 | } 34 | //this.test() 35 | } 36 | 37 | async decorateImage(processedImage: Buffer, data:TweetRequest): Promise { 38 | registerFont(path.join(__dirname, './fonts/retro-computer.ttf'), { family: 'RetroComputer' }); 39 | 40 | const canvasWidth = 1200; 41 | const canvasHeight = 1200; 42 | const canvas = createCanvas(canvasWidth, canvasHeight); 43 | const ctx = canvas.getContext('2d'); 44 | 45 | ctx.fillStyle = '#131415'; 46 | ctx.fillRect(0, 0, canvasWidth, canvasHeight); 47 | 48 | const type = data.eventType 49 | const color = type === 'bids' ? 'rgba(142, 111, 182, 1)' : 50 | type === 'sales' ? 'rgba(99, 133, 150, 1)' : 51 | type === 'offers' ? 'rgba(149, 85, 79, 1)' : 52 | 'rgba(99, 133, 150, 1)'; 53 | 54 | const punkWidth = canvasWidth / 2; 55 | const punkHeight = canvasHeight / 2; 56 | 57 | const bleed = 30 * 2; 58 | 59 | const lowerThird = ((canvasHeight / 3) * 2) - (bleed * 2) + 20; 60 | 61 | const font = (size: number) => `normal ${size}px RetroComputer`; 62 | 63 | ctx.fillStyle = color; 64 | ctx.fillRect(bleed, bleed, canvasWidth - (bleed * 2), lowerThird); 65 | 66 | // Line 1 (left side) 67 | const line1 = 'CryptoPhunk'; 68 | const line1Pos = lowerThird + (bleed * 2); 69 | ctx.textBaseline = 'top'; 70 | ctx.font = font(36); 71 | ctx.fillStyle = '#FFFFFF'; 72 | ctx.fillText( 73 | line1, 74 | bleed, 75 | line1Pos 76 | ); 77 | 78 | // Line 2 (left side) 79 | ctx.textBaseline = 'top'; 80 | ctx.font = font(120); 81 | ctx.fillStyle = color || '#FF04B4'; 82 | ctx.fillText( 83 | data.tokenId, 84 | bleed - 5, 85 | line1Pos + 20 86 | ); 87 | 88 | // Line 3 (right side) 89 | const punkTraits = punkAttributes[data.tokenId]; 90 | const punkData = this.getTraits(punkTraits); 91 | const sex = punkData.traits[0]; 92 | 93 | const line3Pos = lowerThird + (bleed * 2); 94 | const line3_1 = `${punkData.sex} phunks`; 95 | ctx.font = font(24); 96 | ctx.textAlign = 'right'; 97 | ctx.fillStyle = '#FFFFFF'; 98 | ctx.fillText( 99 | line3_1, 100 | canvasWidth - bleed, 101 | line3Pos 102 | ); 103 | 104 | const line3_1Width = (ctx.measureText(line3_1).width + 8); 105 | 106 | const line3_2 = `${sex.value}`; 107 | ctx.font = font(24); 108 | ctx.textAlign = 'right'; 109 | ctx.fillStyle = color || '#ff04b4'; 110 | ctx.fillText( 111 | line3_2, 112 | canvasWidth - bleed - line3_1Width, 113 | line3Pos 114 | ); 115 | 116 | const line3_2Width = (ctx.measureText(line3_2).width + 8); 117 | 118 | const line3_3 = `One of`; 119 | ctx.font = font(24); 120 | ctx.textAlign = 'right'; 121 | ctx.fillStyle = '#FFFFFF'; 122 | ctx.fillText( 123 | line3_3, 124 | canvasWidth - bleed - line3_1Width - line3_2Width, 125 | line3Pos 126 | ); 127 | 128 | const line4Pos = lowerThird + (bleed * 2) + 40; 129 | const line4_1 = `Trait${punkData.traits?.length > 2 ? 's' : ''}` 130 | ctx.font = font(24); 131 | ctx.textAlign = 'right'; 132 | ctx.fillStyle = '#FFFFFF'; 133 | ctx.fillText( 134 | line4_1, 135 | canvasWidth - bleed, 136 | line4Pos 137 | ); 138 | 139 | const line4_1Width = (ctx.measureText(line4_1).width + 8); 140 | 141 | const line4_2 = `${punkData.traitCount}`; 142 | ctx.font = font(24); 143 | ctx.textAlign = 'right'; 144 | ctx.fillStyle = color || '#ff04b4'; 145 | ctx.fillText( 146 | line4_2, 147 | canvasWidth - bleed - line4_1Width, 148 | line4Pos 149 | ); 150 | 151 | let traitsPos = line4Pos + 30; 152 | for (const trait of punkData.traits) { 153 | 154 | if (trait.label !== punkData.sex) { 155 | 156 | const lineTrait_1 = `${(trait.value * 100) / 10000}%`; 157 | ctx.font = font(20); 158 | ctx.textAlign = 'right'; 159 | ctx.fillStyle = '#FFFFFF'; 160 | ctx.fillText( 161 | lineTrait_1, 162 | canvasWidth - bleed, 163 | traitsPos 164 | ); 165 | 166 | const lineTrait_1Width = (ctx.measureText(lineTrait_1).width + 8); 167 | 168 | const lineTrait_2 = `${trait.label}`; 169 | ctx.font = font(20); 170 | ctx.textAlign = 'right'; 171 | ctx.fillStyle = color || '#FF04B4'; 172 | ctx.fillText( 173 | lineTrait_2, 174 | canvasWidth - bleed - lineTrait_1Width, 175 | traitsPos 176 | ); 177 | } 178 | 179 | traitsPos = traitsPos + 30; 180 | } 181 | 182 | await new Promise((resolve, _) => { 183 | svg2img(path.join(__dirname, './images/logo-phunk.svg'), function(error, buffer) { 184 | 185 | loadImage(buffer).then(image => { 186 | ctx.drawImage( 187 | image, 188 | bleed, 189 | canvasHeight - bleed - 20 190 | ); 191 | resolve(_) 192 | }) 193 | }); 194 | }) 195 | const image = await loadImage(processedImage) 196 | 197 | ctx.drawImage( 198 | image, 199 | (canvasWidth / 2) - (punkWidth / 2) - (bleed / 2), 200 | lowerThird - punkHeight + bleed, 201 | punkWidth, 202 | punkHeight 203 | ); 204 | 205 | return canvas.toBuffer('image/png') 206 | } 207 | 208 | 209 | getTraits(punkTraits: any): any { 210 | 211 | const values = {'Female':3840,'Earring':2459,'Green Eye Shadow':271,'Blonde Bob':147,'Male':6039,'Smile':238,'Mohawk':441,'Wild Hair':447,'Pipe':317,'Nerd Glasses':572,'Goat':295,'Big Shades':535,'Purple Eye Shadow':262,'Half Shaved':147,'Do-rag':300,'Clown Eyes Blue':384,'Spots':124,'Wild White Hair':136,'Messy Hair':460,'Luxurious Beard':286,'Big Beard':146,'Clown Nose':212,'Police Cap':203,'Blue Eye Shadow':266,'Straight Hair Dark':148,'Black Lipstick':617,'Clown Eyes Green':382,'Purple Lipstick':655,'Blonde Short':129,'Straight Hair Blonde':144,'Pilot Helmet':54,'Hot Lipstick':696,'Regular Shades':527,'Stringy Hair':463,'Small Shades':378,'Frown':261,'Eye Mask':293,'Muttonchops':303,'Bandana':481,'Horned Rim Glasses':535,'Crazy Hair':414,'Classic Shades':502,'Handlebars':263,'Mohawk Dark':429,'Dark Hair':157,'Peak Spike':303,'Normal Beard Black':289,'Cap':351,'VR':332,'Frumpy Hair':442,'Cigarette':961,'Normal Beard':292,'Red Mohawk':147,'Shaved Head':300,'Chinstrap':282,'Mole':644,'Knitted Cap':419,'Fedora':186,'Shadow Beard':526,'Straight Hair':151,'Hoodie':259,'Eye Patch':461,'Headband':406,'Cowboy Hat':142,'Tassle Hat':178,'3D Glasses':286,'Mustache':288,'Vape':272,'Choker':48,'Pink With Hat':95,'Welding Goggles':86,'Vampire Hair':147,'Mohawk Thin':441,'Tiara':55,'Zombie':88,'Front Beard Dark':260,'Cap Forward':254,'Gold Chain':169,'Purple Hair':165,'Beanie':44,'Clown Hair Green':148,'Pigtails':94,'Silver Chain':156,'Front Beard':273,'Rosy Cheeks':128,'Orange Side':68,'Wild Blonde':144,'Buck Teeth':78,'Top Hat':115,'Medical Mask':175,'Ape':24,'Alien':9}; 212 | 213 | // We want to have sex first 214 | punkTraits.sort(p => p.k === "Sex" ? -1 : 1) 215 | punkTraits = punkTraits.map(p => p.v).filter(p => values.hasOwnProperty(p)) 216 | 217 | return { 218 | sex: punkTraits[0], 219 | traits: punkTraits.map((trait) => ({ label: trait, value: values[trait] })), 220 | traitCount: punkTraits.length - 1, 221 | }; 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { DAORoleConfigurationDto } from "./extensions/dao/models"; 2 | import { BlurIOBasicParser } from "./parsers/blur.io.basic.parser"; 3 | import { BlurIOSalesParser } from "./parsers/blur.io.sales.parser"; 4 | import { BlurIOSweepParser } from "./parsers/blur.io.sweep.parser"; 5 | import { CargoParser } from "./parsers/cargo.parser"; 6 | import { LooksRareParser } from "./parsers/looksrare.parser"; 7 | import { LooksRareV2Parser } from "./parsers/looksrare.v2.parser"; 8 | import { NFTXParser } from "./parsers/nftx.parser"; 9 | import { NotLarvaLabsParser } from "./parsers/notlarvalabs.parser"; 10 | import { OpenSeaSeaportParser } from "./parsers/opensea.seaport.parser"; 11 | import { OpenSeaWyvernParser } from "./parsers/opensea.wyvern.parser"; 12 | import { LogParser } from "./parsers/parser.definition"; 13 | import { RaribleParser } from "./parsers/rarible.parser"; 14 | import { X2Y2Parser } from "./parsers/x2y2.parser"; 15 | 16 | export const config = { 17 | // Contract Address ======================================== // 18 | arcade_api_key: '4C77emHoAhekTX2Tf9DMHIRhTn39E2zKQDGTyV1ExWRaNzslW', 19 | contract_address: '0xf07468ead8cf26c752c676e43c814fee9c8cf402', 20 | nftx_vault_contract_address: '0xB39185e33E8c28e0BB3DbBCe24DA5dEA6379Ae91', 21 | // Enter the block where your contract has been created 22 | statistic_initial_block: 18035326, 23 | // 24 | discord_channels: '919681244537716767,968448656221011981', 25 | discord_client_id: '1139547496033558561', 26 | discord_guild_ids: '880485569652740136,968448656221011978', 27 | dao_requires_encryption_key: false, 28 | dao_roles: [ 29 | /* 30 | { 31 | guildId: '880485569652740136', 32 | roleId: '1157766800629563452', 33 | gracePeriod: 60*60*24, // in seconds (1 day) 34 | minOwnedCount: 1, 35 | minOwnedTime: 30, // in days 36 | disallowAll: false, 37 | }, 38 | */ 39 | 40 | { 41 | guildId: '968448656221011978', 42 | roleId: '1190002144544305262', 43 | minOwnedCount: 1, 44 | minOwnedTime: 30, // in days 45 | }, 46 | 47 | { 48 | guildId: '880485569652740136', 49 | roleId: '1170695892723056650', 50 | specificTrait: { 51 | count: 6 52 | } 53 | }, 54 | /* 55 | { 56 | guildId: '880485569652740136', 57 | roleId: '1158041885454127284', 58 | minted: true 59 | }, 60 | { 61 | guildId: '880485569652740136', 62 | roleId: '1170695892723056650', 63 | specificTrait: { 64 | traitType: 'Eyes', 65 | traitValue: 'Big Shades' 66 | } 67 | }, 68 | */ 69 | /* 70 | { 71 | guildId: '880485569652740136', 72 | roleId: '1175862565490921542', 73 | twitter: { 74 | verified: true, 75 | age: 60*60*24*30, // in seconds (1 month) 76 | } 77 | } 78 | */ 79 | ] as DAORoleConfigurationDto[], 80 | discord_empty_wallet_gifs: ['https://media.tenor.com/J3mNIbj6A4wAAAAd/empty-shelves-john-travolta.gif', 'https://media.tenor.com/NteLNqDJB2QAAAAd/out-of-stock-this-is-happening.gif'], 81 | // 82 | // uncomment the 2 lines above to use local images instead of retrieving images from ipfs for each tweet 83 | use_local_images: true, 84 | local_image_path: './wrapped_punks/punk', 85 | use_forced_remote_image_path: false, 86 | forced_remote_image_path: 'https://cryptopunks.app/public/images/cryptopunks/punk.png', 87 | enable_flashbot_detection: true, 88 | // 89 | // this is a configuration for the phunk bid demo extension 90 | local_bids_image_path: './bids_images/Phunk_', 91 | discord_owned_tokens_image_path: 'http://70.34.216.182/token_images/phunk.png', 92 | discord_footer_text: 'FLIP!', 93 | // this is a configuration for the phunk auction house demo extension 94 | local_auction_image_path: './auction_images/phunk', 95 | token_metadata_cache_path: './token_metadatas_cache', 96 | // 97 | // Fiat Conversion Currency ================================ // 98 | // Available Options: ====================================== // 99 | // usd, aud, gbp, eur, cad, jpy, cny ======================= // 100 | currency: 'usd', 101 | // Message ================================================= // 102 | // Available Parameters: =================================== // 103 | // ==================== Token ID of transfered NFT // 104 | // ================= Value of transactions in eth // 105 | // =============== Value of transactions in fiat // 106 | // =========================== The transaction hash // 107 | // ===================================== From address // 108 | // ========================================= To address // 109 | ownedTokensMessageDiscord: 'Here are the tokens owned by the wallet(s): !\n\n-- Indexing in progress, last event indexed: ``', 110 | graphStatisticsMessageDiscord: 'Here is the graph you requested (wallet: `)`!\n\n-- Indexing in progress, last event indexed: ``', 111 | userStatisticsMessageDiscord: 'Hey, here are the stats you requested about `` !\n\n⏳ It holded a Cryptophunks for the first time days ago.\n💰 It executed transactions involving phunks with a total volume of Ξ.\n🧮 It is currently holding tokens.\n\n-- Indexing in progress, last event indexed: ``', 112 | globalStatisticsMessageDiscord: 'Hey, here are the volume per platform (time window: ) ! 💰\n\n``````\n— Indexing in progress, last event indexed: ``', 113 | saleMessageDiscord: '[Phunk #]() was flipped for [ ()](>)\nfrom: [](https://notlarvalabs.com/cryptophunks/phunkbox?address=)\nto: [](https://notlarvalabs.com/cryptophunks/phunkbox?address=)', 114 | saleMessage: '🚨 Cryptophunks # was sold for 💰 ()\n\nfrom: \nto: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\nhttps://looksrare.org/collections/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\n', 115 | bidMessageDiscord: '[Phunk #]() has a bid for [ ()](>)\nfrom: [](>)', 116 | bidMessage: '🚨 Cryptophunks # received a bid for 💰 ()\n\nfrom: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\nhttps://looksrare.org/collections/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\n', 117 | flywheelMessageDiscord: '🚨 Cryptophunks # has been sold to the auction flywheel for 💰 ()\n\nfrom: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\nhttps://looksrare.org/collections/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\nhttps://www.phunks.pro/\n', 118 | flywheelMessage: '🚨 Cryptophunks # has been sold to the auction flywheel for 💰 ()\n\nfrom: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\nhttps://looksrare.org/collections/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\nhttps://www.phunks.pro/\n', 119 | auctionMessageDiscord: '🚨 Cryptophunks # has been auctioned for 💰 ()\n\nto: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\n\n', 120 | auctionMessage: '🚨 Cryptophunks # has been auctioned for 💰 ()\n\nto: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\n\n', 121 | loanMessage: '🚨 Cryptophunks # has been auctioned for 💰 ()\n\nto: \n\nhttps://etherscan.io/tx/\nhttps://opensea.io/assets/0xf07468ead8cf26c752c676e43c814fee9c8cf402/\n\n', 122 | // Prefer ENS over 0x address (Uses more Alchemy requests) = // 123 | // Available Options: ====================================== // 124 | // true, false ============================================= // 125 | ens: true, 126 | // Include free mints in tweets ============================ // 127 | // Available Options: ====================================== // 128 | // true, false ============================================= // 129 | includeFreeMint: false, 130 | gifModuleMentionnedUserId: 1540024208255754241, 131 | parsers: [ 132 | new OpenSeaWyvernParser(), 133 | new OpenSeaSeaportParser(), 134 | new LooksRareParser(), 135 | new LooksRareV2Parser(), 136 | new NotLarvaLabsParser(), 137 | new X2Y2Parser(), 138 | new RaribleParser(), 139 | new CargoParser(), 140 | new NFTXParser(), 141 | new BlurIOBasicParser(), 142 | new BlurIOSalesParser(), 143 | new BlurIOSweepParser(), // must be the last blurio parsers 144 | ] as LogParser[], 145 | daoModuleListenAddress: 'localhost', 146 | twitterAPIRedirectURL: `http://localhost:3000/twitter` 147 | }; 148 | -------------------------------------------------------------------------------- /src/erc721sales.service.ts: -------------------------------------------------------------------------------- 1 | import erc721abi from './abi/erc721.json'; 2 | import fetch from "node-fetch"; 3 | import { promises as fs } from 'fs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { HttpService } from '@nestjs/axios'; 6 | import { AbiCoder, JsonRpcProvider, Transaction, TransactionDescription, TransactionReceipt, ethers } from 'ethers'; 7 | import { hexToNumberString } from 'web3-utils'; 8 | 9 | import dotenv from 'dotenv'; 10 | dotenv.config(); 11 | 12 | import { config } from './config'; 13 | import { BaseService, TweetRequest } from './base.service'; 14 | import { createLogger } from './logging.utils'; 15 | 16 | const logger = createLogger('erc721sales.service') 17 | 18 | // This can be an array if you want to filter by multiple topics 19 | // 'Transfer' topic 20 | const topics = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; 21 | 22 | @Injectable() 23 | export class Erc721SalesService extends BaseService { 24 | 25 | provider = this.getWeb3Provider(); 26 | currentBlock:number = -1 27 | 28 | constructor( 29 | protected readonly http: HttpService, 30 | ) { 31 | super(http) 32 | 33 | if (!global.doNotStartAutomatically) { 34 | this.startProvider() 35 | } 36 | //this.test() 37 | } 38 | 39 | async test() { 40 | const tokenContract = new ethers.Contract(this.getContractAddress(), erc721abi, this.provider); 41 | let filter = tokenContract.filters.Transfer(); 42 | const events = await tokenContract.queryFilter(filter, 43 | 18247007, 44 | 18247007) 45 | for (let e of events) { 46 | const t = await this.getTransactionDetails(e) 47 | this.dispatch(t) 48 | } 49 | } 50 | 51 | async startProvider() { 52 | 53 | this.initDiscordClient(); 54 | 55 | const CHUNK_SIZE = 10 56 | const tokenContract = new ethers.Contract(this.getContractAddress(), erc721abi, this.provider); 57 | const filter = [topics]; 58 | 59 | try { 60 | this.currentBlock = parseInt(await fs.readFile(this.getPositionFile(), { encoding: 'utf8' })) 61 | } catch (err) { 62 | } 63 | if (isNaN(this.currentBlock) || this.currentBlock <= 0) { 64 | this.currentBlock = await this.getWeb3Provider().getBlockNumber() 65 | await this.updatePosition(this.currentBlock) 66 | } 67 | console.log(`position: ${this.currentBlock}`) 68 | 69 | let retryCount = 0 70 | let latestTweetedBlock = 0 71 | let latestTweetedTx = '' 72 | while (true) { 73 | try { 74 | const latestAvailableBlock = await this.provider.getBlockNumber() 75 | if (this.currentBlock >= latestAvailableBlock) { 76 | logger.info(`latest block reached (${latestAvailableBlock}), waiting the next available block...`) 77 | await delay(10000) 78 | continue 79 | } 80 | 81 | console.log(`checking ${this.currentBlock}`) 82 | const events = await tokenContract.queryFilter(filter, this.currentBlock, this.currentBlock + CHUNK_SIZE) 83 | 84 | for (let event of events) { 85 | latestTweetedBlock = event.blockNumber 86 | latestTweetedTx = event.transactionHash 87 | await this.handleEvent(event) 88 | } 89 | 90 | this.currentBlock += CHUNK_SIZE 91 | if (this.currentBlock > latestAvailableBlock) this.currentBlock = latestAvailableBlock + 1 92 | await this.updatePosition(latestAvailableBlock) 93 | } catch (err) { 94 | console.log(err) 95 | retryCount++ 96 | if (retryCount > 5) { 97 | console.log(`stop retrying, failing on ${latestTweetedTx}, moving to next block`) 98 | this.currentBlock = latestTweetedBlock + 1 99 | retryCount = 0 100 | } 101 | } 102 | } 103 | 104 | } 105 | 106 | async handleEvent(event) { 107 | const res = await this.getTransactionDetails(event, false, true) 108 | if (!res) return 109 | // Only tweet transfers with value (Ignore w2w transfers) 110 | if (res?.ether || res?.alternateValue) this.dispatch(res); 111 | // If free mint is enabled we can tweet 0 value 112 | else if (config.includeFreeMint) this.tweet(res);; 113 | } 114 | 115 | async getTransactionDetails(tx: any, ignoreENS:boolean=false, ignoreContracts:boolean=true): Promise { 116 | // uncomment this to test a specific transaction 117 | // if (tx.transactionHash !== '0xcee5c725e2234fd0704e1408cdf7f71d881e67f8bf5d6696a98fdd7c0bcf52f3') return; 118 | 119 | let tokenId: string; 120 | let retryCount: number = 0 121 | 122 | while (true) { 123 | try { 124 | 125 | // Get addresses of seller / buyer from topics 126 | const coder = AbiCoder.defaultAbiCoder() 127 | let from = coder.decode(['address'], tx?.topics[1])[0]; 128 | let to = coder.decode(['address'], tx?.topics[2])[0]; 129 | 130 | // ignore internal transfers to contract, another transfer event will handle this 131 | // transaction afterward (the one that'll go to the buyer wallet) 132 | const code = await this.provider.getCode(to) 133 | // the ignoreContracts flag make the MEV bots like transaction ignored by the twitter 134 | // bot, but not for statistics 135 | if (to !== config.nftx_vault_contract_address && code !== '0x' && ignoreContracts) { 136 | logger.info(`contract detected for ${tx.transactionHash} event index ${tx.index}`) 137 | return 138 | } 139 | 140 | // not an erc721 transfer 141 | // Get transaction receipt 142 | const receipt: TransactionReceipt = await this.provider.getTransactionReceipt(tx.transactionHash); 143 | 144 | // Get tokenId from topics 145 | tokenId = this.getTokenId(tx, receipt); 146 | if (!tokenId) break 147 | 148 | // Get transaction hash 149 | const { transactionHash } = tx; 150 | const isMint = BigInt(from) === BigInt(0); 151 | 152 | // Get transaction 153 | const transaction = await this.provider.getTransaction(transactionHash); 154 | const block = await this.provider.getBlock(transaction.blockNumber) 155 | const transactionDate = block.date.toISOString() 156 | logger.info(`handling ${transactionHash} token ${tokenId} log ${tx.index} — ${transactionDate} - from ${tx.blockNumber}`) 157 | 158 | const { value } = transaction; 159 | let ether = ethers.formatEther(value.toString()); 160 | 161 | // Get token image 162 | const imageUrl = await this.getImageUri(tokenId) 163 | 164 | // If ens is configured, get ens addresses 165 | let ensTo: string; 166 | let ensFrom: string; 167 | if (config.ens && !ignoreENS) { 168 | ensTo = await this.provider.lookupAddress(`${to}`); 169 | ensFrom = await this.provider.lookupAddress(`${from}`); 170 | } 171 | 172 | // Set the values for address to & from -- Shorten non ens 173 | const initialFrom = from 174 | const initialTo = to 175 | to = config.ens && !ignoreENS ? (ensTo ? ensTo : this.shortenAddress(to)) : this.shortenAddress(to); 176 | from = (isMint && config.includeFreeMint) ? 'Mint' : config.ens ? (ensFrom ? ensFrom : this.shortenAddress(from)) : this.shortenAddress(from); 177 | 178 | // Create response object 179 | const tweetRequest: TweetRequest = { 180 | logIndex: tx.index, 181 | eventType: isMint ? 'mint' : 'sale', 182 | initialFrom, 183 | initialTo, 184 | from, 185 | erc20Token: 'ethereum', 186 | to, 187 | tokenId, 188 | ether: parseFloat(ether), 189 | transactionHash, 190 | transactionDate, 191 | alternateValue: 0, 192 | platform: 'unknown', 193 | }; 194 | 195 | // If the image was successfully obtained 196 | if (imageUrl) tweetRequest.imageUrl = imageUrl; 197 | 198 | // Try to use custom parsers 199 | for (let parser of config.parsers) { 200 | const result = await parser.parseLogs(transaction, receipt.logs, tokenId) 201 | if (result) { 202 | tweetRequest.alternateValue = result 203 | tweetRequest.platform = parser.platform 204 | break 205 | } 206 | } 207 | 208 | if (this.getForcedPlatform()) { 209 | tweetRequest.platform = this.getForcedPlatform() 210 | } 211 | 212 | if (transaction.to === '0x941A6d105802CCCaa06DE58a13a6F49ebDCD481C' && !tweetRequest.alternateValue) { 213 | // nftx swap of "inner token" that weren't bought in the same transaction ignore this 214 | logger.info(`nftx swap detected without ETH buy, ignoring ${tx.transactionHash} event index ${tx.index}`) 215 | return 216 | } 217 | return tweetRequest 218 | 219 | } catch (err) { 220 | logger.info(`${tokenId} failed to send, retryCount: ${retryCount}`, err); 221 | retryCount++ 222 | if (retryCount >= 10) { 223 | logger.info("retried 10 times, giving up") 224 | return null; 225 | } 226 | logger.info(`will retry after a delay ${retryCount}...`) 227 | await new Promise( resolve => setTimeout(resolve, 500*retryCount) ) 228 | } 229 | } 230 | } 231 | 232 | getTokenId(tx:any, receipt:TransactionReceipt) { 233 | return hexToNumberString(tx?.topics[3]) 234 | } 235 | 236 | getContractAddress() { 237 | return config.contract_address 238 | } 239 | 240 | getForcedPlatform() { 241 | return undefined 242 | } 243 | 244 | getPositionFile() { 245 | return 'erc721.position.txt' 246 | } 247 | 248 | async getImageUri(tokenId) { 249 | return config.use_forced_remote_image_path ? 250 | config.forced_remote_image_path.replace(new RegExp('', 'g'), tokenId.padStart(4, '0')) 251 | : config.use_local_images 252 | ? `${config.local_image_path}${tokenId.padStart(4, '0')}.png` 253 | : await this.getTokenMetadata(tokenId); 254 | } 255 | 256 | } 257 | 258 | function delay(ms: number) { 259 | return new Promise( resolve => setTimeout(resolve, ms) ); 260 | } -------------------------------------------------------------------------------- /src/abi/notlarvalabs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "initialPhunksAddress", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "uint256", 19 | "name": "phunkIndex", 20 | "type": "uint256" 21 | }, 22 | { 23 | "indexed": false, 24 | "internalType": "uint256", 25 | "name": "value", 26 | "type": "uint256" 27 | }, 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "fromAddress", 32 | "type": "address" 33 | } 34 | ], 35 | "name": "PhunkBidEntered", 36 | "type": "event" 37 | }, 38 | { 39 | "anonymous": false, 40 | "inputs": [ 41 | { 42 | "indexed": true, 43 | "internalType": "uint256", 44 | "name": "phunkIndex", 45 | "type": "uint256" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "uint256", 50 | "name": "value", 51 | "type": "uint256" 52 | }, 53 | { 54 | "indexed": true, 55 | "internalType": "address", 56 | "name": "fromAddress", 57 | "type": "address" 58 | } 59 | ], 60 | "name": "PhunkBidWithdrawn", 61 | "type": "event" 62 | }, 63 | { 64 | "anonymous": false, 65 | "inputs": [ 66 | { 67 | "indexed": true, 68 | "internalType": "uint256", 69 | "name": "phunkIndex", 70 | "type": "uint256" 71 | }, 72 | { 73 | "indexed": false, 74 | "internalType": "uint256", 75 | "name": "value", 76 | "type": "uint256" 77 | }, 78 | { 79 | "indexed": true, 80 | "internalType": "address", 81 | "name": "fromAddress", 82 | "type": "address" 83 | }, 84 | { 85 | "indexed": true, 86 | "internalType": "address", 87 | "name": "toAddress", 88 | "type": "address" 89 | } 90 | ], 91 | "name": "PhunkBought", 92 | "type": "event" 93 | }, 94 | { 95 | "anonymous": false, 96 | "inputs": [ 97 | { 98 | "indexed": true, 99 | "internalType": "uint256", 100 | "name": "phunkIndex", 101 | "type": "uint256" 102 | } 103 | ], 104 | "name": "PhunkNoLongerForSale", 105 | "type": "event" 106 | }, 107 | { 108 | "anonymous": false, 109 | "inputs": [ 110 | { 111 | "indexed": true, 112 | "internalType": "uint256", 113 | "name": "phunkIndex", 114 | "type": "uint256" 115 | }, 116 | { 117 | "indexed": false, 118 | "internalType": "uint256", 119 | "name": "minValue", 120 | "type": "uint256" 121 | }, 122 | { 123 | "indexed": true, 124 | "internalType": "address", 125 | "name": "toAddress", 126 | "type": "address" 127 | } 128 | ], 129 | "name": "PhunkOffered", 130 | "type": "event" 131 | }, 132 | { 133 | "inputs": [ 134 | { 135 | "internalType": "uint256", 136 | "name": "phunkIndex", 137 | "type": "uint256" 138 | }, 139 | { 140 | "internalType": "uint256", 141 | "name": "minPrice", 142 | "type": "uint256" 143 | } 144 | ], 145 | "name": "acceptBidForPhunk", 146 | "outputs": [], 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "inputs": [ 152 | { 153 | "internalType": "uint256", 154 | "name": "phunkIndex", 155 | "type": "uint256" 156 | } 157 | ], 158 | "name": "buyPhunk", 159 | "outputs": [], 160 | "stateMutability": "payable", 161 | "type": "function" 162 | }, 163 | { 164 | "inputs": [ 165 | { 166 | "internalType": "uint256", 167 | "name": "phunkIndex", 168 | "type": "uint256" 169 | } 170 | ], 171 | "name": "enterBidForPhunk", 172 | "outputs": [], 173 | "stateMutability": "payable", 174 | "type": "function" 175 | }, 176 | { 177 | "inputs": [ 178 | { 179 | "internalType": "uint256", 180 | "name": "phunkIndex", 181 | "type": "uint256" 182 | }, 183 | { 184 | "internalType": "uint256", 185 | "name": "minSalePriceInWei", 186 | "type": "uint256" 187 | } 188 | ], 189 | "name": "offerPhunkForSale", 190 | "outputs": [], 191 | "stateMutability": "nonpayable", 192 | "type": "function" 193 | }, 194 | { 195 | "inputs": [ 196 | { 197 | "internalType": "uint256", 198 | "name": "phunkIndex", 199 | "type": "uint256" 200 | }, 201 | { 202 | "internalType": "uint256", 203 | "name": "minSalePriceInWei", 204 | "type": "uint256" 205 | }, 206 | { 207 | "internalType": "address", 208 | "name": "toAddress", 209 | "type": "address" 210 | } 211 | ], 212 | "name": "offerPhunkForSaleToAddress", 213 | "outputs": [], 214 | "stateMutability": "nonpayable", 215 | "type": "function" 216 | }, 217 | { 218 | "inputs": [ 219 | { 220 | "internalType": "address", 221 | "name": "", 222 | "type": "address" 223 | } 224 | ], 225 | "name": "pendingWithdrawals", 226 | "outputs": [ 227 | { 228 | "internalType": "uint256", 229 | "name": "", 230 | "type": "uint256" 231 | } 232 | ], 233 | "stateMutability": "view", 234 | "type": "function" 235 | }, 236 | { 237 | "inputs": [ 238 | { 239 | "internalType": "uint256", 240 | "name": "", 241 | "type": "uint256" 242 | } 243 | ], 244 | "name": "phunkBids", 245 | "outputs": [ 246 | { 247 | "internalType": "bool", 248 | "name": "hasBid", 249 | "type": "bool" 250 | }, 251 | { 252 | "internalType": "uint256", 253 | "name": "phunkIndex", 254 | "type": "uint256" 255 | }, 256 | { 257 | "internalType": "address", 258 | "name": "bidder", 259 | "type": "address" 260 | }, 261 | { 262 | "internalType": "uint256", 263 | "name": "value", 264 | "type": "uint256" 265 | } 266 | ], 267 | "stateMutability": "view", 268 | "type": "function" 269 | }, 270 | { 271 | "inputs": [ 272 | { 273 | "internalType": "uint256", 274 | "name": "phunkIndex", 275 | "type": "uint256" 276 | } 277 | ], 278 | "name": "phunkNoLongerForSale", 279 | "outputs": [], 280 | "stateMutability": "nonpayable", 281 | "type": "function" 282 | }, 283 | { 284 | "inputs": [], 285 | "name": "phunksAddress", 286 | "outputs": [ 287 | { 288 | "internalType": "address", 289 | "name": "", 290 | "type": "address" 291 | } 292 | ], 293 | "stateMutability": "view", 294 | "type": "function" 295 | }, 296 | { 297 | "inputs": [ 298 | { 299 | "internalType": "uint256", 300 | "name": "", 301 | "type": "uint256" 302 | } 303 | ], 304 | "name": "phunksOfferedForSale", 305 | "outputs": [ 306 | { 307 | "internalType": "bool", 308 | "name": "isForSale", 309 | "type": "bool" 310 | }, 311 | { 312 | "internalType": "uint256", 313 | "name": "phunkIndex", 314 | "type": "uint256" 315 | }, 316 | { 317 | "internalType": "address", 318 | "name": "seller", 319 | "type": "address" 320 | }, 321 | { 322 | "internalType": "uint256", 323 | "name": "minValue", 324 | "type": "uint256" 325 | }, 326 | { 327 | "internalType": "address", 328 | "name": "onlySellTo", 329 | "type": "address" 330 | } 331 | ], 332 | "stateMutability": "view", 333 | "type": "function" 334 | }, 335 | { 336 | "inputs": [ 337 | { 338 | "internalType": "address", 339 | "name": "newPhunksAddress", 340 | "type": "address" 341 | } 342 | ], 343 | "name": "setPhunksContract", 344 | "outputs": [], 345 | "stateMutability": "nonpayable", 346 | "type": "function" 347 | }, 348 | { 349 | "inputs": [], 350 | "name": "withdraw", 351 | "outputs": [], 352 | "stateMutability": "nonpayable", 353 | "type": "function" 354 | }, 355 | { 356 | "inputs": [ 357 | { 358 | "internalType": "uint256", 359 | "name": "phunkIndex", 360 | "type": "uint256" 361 | } 362 | ], 363 | "name": "withdrawBidForPhunk", 364 | "outputs": [], 365 | "stateMutability": "nonpayable", 366 | "type": "function" 367 | } 368 | ] -------------------------------------------------------------------------------- /src/abi/erc721.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "constant": false, 5 | "inputs": 6 | [ 7 | { 8 | "internalType": "address", 9 | "name": "to", 10 | "type": "address" 11 | }, 12 | { 13 | "internalType": "uint256", 14 | "name": "tokenId", 15 | "type": "uint256" 16 | } 17 | ], 18 | "name": "approve", 19 | "outputs": 20 | [], 21 | "payable": false, 22 | "stateMutability": "nonpayable", 23 | "type": "function" 24 | }, 25 | { 26 | "inputs": 27 | [ 28 | { 29 | "internalType": "uint256", 30 | "name": "tokenId", 31 | "type": "uint256" 32 | } 33 | ], 34 | "name": "tokenURI", 35 | "outputs": 36 | [ 37 | { 38 | "internalType": "string", 39 | "name": "", 40 | "type": "string" 41 | } 42 | ], 43 | "stateMutability": "view", 44 | "type": "function" 45 | }, 46 | { 47 | "inputs": 48 | [], 49 | "name": "totalSupply", 50 | "outputs": 51 | [ 52 | { 53 | "internalType": "uint256", 54 | "name": "", 55 | "type": "uint256" 56 | } 57 | ], 58 | "stateMutability": "view", 59 | "type": "function" 60 | }, 61 | { 62 | "constant": false, 63 | "inputs": 64 | [ 65 | { 66 | "internalType": "address", 67 | "name": "to", 68 | "type": "address" 69 | }, 70 | { 71 | "internalType": "uint256", 72 | "name": "tokenId", 73 | "type": "uint256" 74 | } 75 | ], 76 | "name": "mint", 77 | "outputs": 78 | [], 79 | "payable": false, 80 | "stateMutability": "nonpayable", 81 | "type": "function" 82 | }, 83 | { 84 | "constant": false, 85 | "inputs": 86 | [ 87 | { 88 | "internalType": "address", 89 | "name": "from", 90 | "type": "address" 91 | }, 92 | { 93 | "internalType": "address", 94 | "name": "to", 95 | "type": "address" 96 | }, 97 | { 98 | "internalType": "uint256", 99 | "name": "tokenId", 100 | "type": "uint256" 101 | } 102 | ], 103 | "name": "safeTransferFrom", 104 | "outputs": 105 | [], 106 | "payable": false, 107 | "stateMutability": "nonpayable", 108 | "type": "function" 109 | }, 110 | { 111 | "constant": false, 112 | "inputs": 113 | [ 114 | { 115 | "internalType": "address", 116 | "name": "from", 117 | "type": "address" 118 | }, 119 | { 120 | "internalType": "address", 121 | "name": "to", 122 | "type": "address" 123 | }, 124 | { 125 | "internalType": "uint256", 126 | "name": "tokenId", 127 | "type": "uint256" 128 | }, 129 | { 130 | "internalType": "bytes", 131 | "name": "_data", 132 | "type": "bytes" 133 | } 134 | ], 135 | "name": "safeTransferFrom", 136 | "outputs": 137 | [], 138 | "payable": false, 139 | "stateMutability": "nonpayable", 140 | "type": "function" 141 | }, 142 | { 143 | "constant": false, 144 | "inputs": 145 | [ 146 | { 147 | "internalType": "address", 148 | "name": "to", 149 | "type": "address" 150 | }, 151 | { 152 | "internalType": "bool", 153 | "name": "approved", 154 | "type": "bool" 155 | } 156 | ], 157 | "name": "setApprovalForAll", 158 | "outputs": 159 | [], 160 | "payable": false, 161 | "stateMutability": "nonpayable", 162 | "type": "function" 163 | }, 164 | { 165 | "constant": false, 166 | "inputs": 167 | [ 168 | { 169 | "internalType": "address", 170 | "name": "from", 171 | "type": "address" 172 | }, 173 | { 174 | "internalType": "address", 175 | "name": "to", 176 | "type": "address" 177 | }, 178 | { 179 | "internalType": "uint256", 180 | "name": "tokenId", 181 | "type": "uint256" 182 | } 183 | ], 184 | "name": "transferFrom", 185 | "outputs": 186 | [], 187 | "payable": false, 188 | "stateMutability": "nonpayable", 189 | "type": "function" 190 | }, 191 | { 192 | "inputs": 193 | [], 194 | "payable": false, 195 | "stateMutability": "nonpayable", 196 | "type": "constructor" 197 | }, 198 | { 199 | "anonymous": false, 200 | "inputs": 201 | [ 202 | { 203 | "indexed": true, 204 | "internalType": "address", 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "internalType": "address", 211 | "name": "to", 212 | "type": "address" 213 | }, 214 | { 215 | "indexed": true, 216 | "internalType": "uint256", 217 | "name": "tokenId", 218 | "type": "uint256" 219 | } 220 | ], 221 | "name": "Transfer", 222 | "type": "event" 223 | }, 224 | { 225 | "anonymous": false, 226 | "inputs": 227 | [ 228 | { 229 | "indexed": true, 230 | "internalType": "address", 231 | "name": "owner", 232 | "type": "address" 233 | }, 234 | { 235 | "indexed": true, 236 | "internalType": "address", 237 | "name": "approved", 238 | "type": "address" 239 | }, 240 | { 241 | "indexed": true, 242 | "internalType": "uint256", 243 | "name": "tokenId", 244 | "type": "uint256" 245 | } 246 | ], 247 | "name": "Approval", 248 | "type": "event" 249 | }, 250 | { 251 | "anonymous": false, 252 | "inputs": 253 | [ 254 | { 255 | "indexed": true, 256 | "internalType": "address", 257 | "name": "owner", 258 | "type": "address" 259 | }, 260 | { 261 | "indexed": true, 262 | "internalType": "address", 263 | "name": "operator", 264 | "type": "address" 265 | }, 266 | { 267 | "indexed": false, 268 | "internalType": "bool", 269 | "name": "approved", 270 | "type": "bool" 271 | } 272 | ], 273 | "name": "ApprovalForAll", 274 | "type": "event" 275 | }, 276 | { 277 | "constant": true, 278 | "inputs": 279 | [ 280 | { 281 | "internalType": "address", 282 | "name": "owner", 283 | "type": "address" 284 | } 285 | ], 286 | "name": "balanceOf", 287 | "outputs": 288 | [ 289 | { 290 | "internalType": "uint256", 291 | "name": "", 292 | "type": "uint256" 293 | } 294 | ], 295 | "payable": false, 296 | "stateMutability": "view", 297 | "type": "function" 298 | }, 299 | { 300 | "constant": true, 301 | "inputs": 302 | [ 303 | { 304 | "internalType": "uint256", 305 | "name": "tokenId", 306 | "type": "uint256" 307 | } 308 | ], 309 | "name": "getApproved", 310 | "outputs": 311 | [ 312 | { 313 | "internalType": "address", 314 | "name": "", 315 | "type": "address" 316 | } 317 | ], 318 | "payable": false, 319 | "stateMutability": "view", 320 | "type": "function" 321 | }, 322 | { 323 | "constant": true, 324 | "inputs": 325 | [ 326 | { 327 | "internalType": "address", 328 | "name": "owner", 329 | "type": "address" 330 | }, 331 | { 332 | "internalType": "address", 333 | "name": "operator", 334 | "type": "address" 335 | } 336 | ], 337 | "name": "isApprovedForAll", 338 | "outputs": 339 | [ 340 | { 341 | "internalType": "bool", 342 | "name": "", 343 | "type": "bool" 344 | } 345 | ], 346 | "payable": false, 347 | "stateMutability": "view", 348 | "type": "function" 349 | }, 350 | { 351 | "constant": true, 352 | "inputs": 353 | [ 354 | { 355 | "internalType": "uint256", 356 | "name": "tokenId", 357 | "type": "uint256" 358 | } 359 | ], 360 | "name": "ownerOf", 361 | "outputs": 362 | [ 363 | { 364 | "internalType": "address", 365 | "name": "", 366 | "type": "address" 367 | } 368 | ], 369 | "payable": false, 370 | "stateMutability": "view", 371 | "type": "function" 372 | }, 373 | { 374 | "constant": true, 375 | "inputs": 376 | [ 377 | { 378 | "internalType": "bytes4", 379 | "name": "interfaceId", 380 | "type": "bytes4" 381 | } 382 | ], 383 | "name": "supportsInterface", 384 | "outputs": 385 | [ 386 | { 387 | "internalType": "bool", 388 | "name": "", 389 | "type": "bool" 390 | } 391 | ], 392 | "payable": false, 393 | "stateMutability": "view", 394 | "type": "function" 395 | } 396 | ] -------------------------------------------------------------------------------- /src/abi/nftxABI.json: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"moduleIndex","type":"uint256"},{"indexed":false,"internalType":"address","name":"eligibilityAddr","type":"address"}],"name":"EligibilityDeployed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bool","name":"enabled","type":"bool"}],"name":"EnableMintUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bool","name":"enabled","type":"bool"}],"name":"EnableRandomRedeemUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bool","name":"enabled","type":"bool"}],"name":"EnableTargetRedeemUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"manager","type":"address"}],"name":"ManagerSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"mintFee","type":"uint256"}],"name":"MintFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256[]","name":"nftIds","type":"uint256[]"},{"indexed":false,"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"indexed":false,"internalType":"address","name":"to","type":"address"}],"name":"Minted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"randomRedeemFee","type":"uint256"}],"name":"RandomRedeemFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256[]","name":"nftIds","type":"uint256[]"},{"indexed":false,"internalType":"uint256[]","name":"specificIds","type":"uint256[]"},{"indexed":false,"internalType":"address","name":"to","type":"address"}],"name":"Redeemed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256[]","name":"nftIds","type":"uint256[]"},{"indexed":false,"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"indexed":false,"internalType":"uint256[]","name":"specificIds","type":"uint256[]"},{"indexed":false,"internalType":"uint256[]","name":"redeemedIds","type":"uint256[]"},{"indexed":false,"internalType":"address","name":"to","type":"address"}],"name":"Swapped","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"targetRedeemFee","type":"uint256"}],"name":"TargetRedeemFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"vaultId","type":"uint256"},{"indexed":false,"internalType":"address","name":"assetAddress","type":"address"},{"indexed":false,"internalType":"bool","name":"is1155","type":"bool"},{"indexed":false,"internalType":"bool","name":"allowAllItems","type":"bool"}],"name":"VaultInit","type":"event"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"address","name":"_assetAddress","type":"address"},{"internalType":"bool","name":"_is1155","type":"bool"},{"internalType":"bool","name":"_allowAllItems","type":"bool"}],"name":"__NFTXVault_init","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"allHoldings","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"allValidNFTs","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"allowAllItems","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"assetAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"moduleIndex","type":"uint256"},{"internalType":"bytes","name":"initData","type":"bytes"}],"name":"deployEligibilityStorage","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"eligibilityStorage","outputs":[{"internalType":"contract INFTXEligibility","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"enableMint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"enableRandomRedeem","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"enableTargetRedeem","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"finalizeVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"flashFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract IERC3156FlashBorrowerUpgradeable","name":"receiver","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"flashLoan","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"is1155","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"manager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"maxFlashLoan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"mint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"mintFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"to","type":"address"}],"name":"mintTo","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"holdingsIndex","type":"uint256"}],"name":"nftIdAt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155BatchReceived","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"randomRedeemFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256[]","name":"specificIds","type":"uint256[]"}],"name":"redeem","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256[]","name":"specificIds","type":"uint256[]"},{"internalType":"address","name":"to","type":"address"}],"name":"redeemTo","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_mintFee","type":"uint256"},{"internalType":"uint256","name":"_randomRedeemFee","type":"uint256"},{"internalType":"uint256","name":"_targetRedeemFee","type":"uint256"}],"name":"setFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_manager","type":"address"}],"name":"setManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"_enableMint","type":"bool"},{"internalType":"bool","name":"_enableRandomRedeem","type":"bool"},{"internalType":"bool","name":"_enableTargetRedeem","type":"bool"}],"name":"setVaultFeatures","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"}],"name":"setVaultMetadata","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"uint256[]","name":"specificIds","type":"uint256[]"}],"name":"swap","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"uint256[]","name":"specificIds","type":"uint256[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapTo","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"targetRedeemFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalHoldings","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vaultFactory","outputs":[{"internalType":"contract INFTXVaultFactory","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vaultId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"}] -------------------------------------------------------------------------------- /src/abi/punks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "type": "function" 14 | }, 15 | { 16 | "constant": true, 17 | "inputs": [ 18 | { 19 | "name": "", 20 | "type": "uint256" 21 | } 22 | ], 23 | "name": "punksOfferedForSale", 24 | "outputs": [ 25 | { 26 | "name": "isForSale", 27 | "type": "bool" 28 | }, 29 | { 30 | "name": "punkIndex", 31 | "type": "uint256" 32 | }, 33 | { 34 | "name": "seller", 35 | "type": "address" 36 | }, 37 | { 38 | "name": "minValue", 39 | "type": "uint256" 40 | }, 41 | { 42 | "name": "onlySellTo", 43 | "type": "address" 44 | } 45 | ], 46 | "payable": false, 47 | "type": "function" 48 | }, 49 | { 50 | "constant": false, 51 | "inputs": [ 52 | { 53 | "name": "punkIndex", 54 | "type": "uint256" 55 | } 56 | ], 57 | "name": "enterBidForPunk", 58 | "outputs": [], 59 | "payable": true, 60 | "type": "function" 61 | }, 62 | { 63 | "constant": true, 64 | "inputs": [], 65 | "name": "totalSupply", 66 | "outputs": [ 67 | { 68 | "name": "", 69 | "type": "uint256" 70 | } 71 | ], 72 | "payable": false, 73 | "type": "function" 74 | }, 75 | { 76 | "constant": false, 77 | "inputs": [ 78 | { 79 | "name": "punkIndex", 80 | "type": "uint256" 81 | }, 82 | { 83 | "name": "minPrice", 84 | "type": "uint256" 85 | } 86 | ], 87 | "name": "acceptBidForPunk", 88 | "outputs": [], 89 | "payable": false, 90 | "type": "function" 91 | }, 92 | { 93 | "constant": true, 94 | "inputs": [], 95 | "name": "decimals", 96 | "outputs": [ 97 | { 98 | "name": "", 99 | "type": "uint8" 100 | } 101 | ], 102 | "payable": false, 103 | "type": "function" 104 | }, 105 | { 106 | "constant": false, 107 | "inputs": [ 108 | { 109 | "name": "addresses", 110 | "type": "address[]" 111 | }, 112 | { 113 | "name": "indices", 114 | "type": "uint256[]" 115 | } 116 | ], 117 | "name": "setInitialOwners", 118 | "outputs": [], 119 | "payable": false, 120 | "type": "function" 121 | }, 122 | { 123 | "constant": false, 124 | "inputs": [], 125 | "name": "withdraw", 126 | "outputs": [], 127 | "payable": false, 128 | "type": "function" 129 | }, 130 | { 131 | "constant": true, 132 | "inputs": [], 133 | "name": "imageHash", 134 | "outputs": [ 135 | { 136 | "name": "", 137 | "type": "string" 138 | } 139 | ], 140 | "payable": false, 141 | "type": "function" 142 | }, 143 | { 144 | "constant": true, 145 | "inputs": [], 146 | "name": "nextPunkIndexToAssign", 147 | "outputs": [ 148 | { 149 | "name": "", 150 | "type": "uint256" 151 | } 152 | ], 153 | "payable": false, 154 | "type": "function" 155 | }, 156 | { 157 | "constant": true, 158 | "inputs": [ 159 | { 160 | "name": "", 161 | "type": "uint256" 162 | } 163 | ], 164 | "name": "punkIndexToAddress", 165 | "outputs": [ 166 | { 167 | "name": "", 168 | "type": "address" 169 | } 170 | ], 171 | "payable": false, 172 | "type": "function" 173 | }, 174 | { 175 | "constant": true, 176 | "inputs": [], 177 | "name": "standard", 178 | "outputs": [ 179 | { 180 | "name": "", 181 | "type": "string" 182 | } 183 | ], 184 | "payable": false, 185 | "type": "function" 186 | }, 187 | { 188 | "constant": true, 189 | "inputs": [ 190 | { 191 | "name": "", 192 | "type": "uint256" 193 | } 194 | ], 195 | "name": "punkBids", 196 | "outputs": [ 197 | { 198 | "name": "hasBid", 199 | "type": "bool" 200 | }, 201 | { 202 | "name": "punkIndex", 203 | "type": "uint256" 204 | }, 205 | { 206 | "name": "bidder", 207 | "type": "address" 208 | }, 209 | { 210 | "name": "value", 211 | "type": "uint256" 212 | } 213 | ], 214 | "payable": false, 215 | "type": "function" 216 | }, 217 | { 218 | "constant": true, 219 | "inputs": [ 220 | { 221 | "name": "", 222 | "type": "address" 223 | } 224 | ], 225 | "name": "balanceOf", 226 | "outputs": [ 227 | { 228 | "name": "", 229 | "type": "uint256" 230 | } 231 | ], 232 | "payable": false, 233 | "type": "function" 234 | }, 235 | { 236 | "constant": false, 237 | "inputs": [], 238 | "name": "allInitialOwnersAssigned", 239 | "outputs": [], 240 | "payable": false, 241 | "type": "function" 242 | }, 243 | { 244 | "constant": true, 245 | "inputs": [], 246 | "name": "allPunksAssigned", 247 | "outputs": [ 248 | { 249 | "name": "", 250 | "type": "bool" 251 | } 252 | ], 253 | "payable": false, 254 | "type": "function" 255 | }, 256 | { 257 | "constant": false, 258 | "inputs": [ 259 | { 260 | "name": "punkIndex", 261 | "type": "uint256" 262 | } 263 | ], 264 | "name": "buyPunk", 265 | "outputs": [], 266 | "payable": true, 267 | "type": "function" 268 | }, 269 | { 270 | "constant": false, 271 | "inputs": [ 272 | { 273 | "name": "to", 274 | "type": "address" 275 | }, 276 | { 277 | "name": "punkIndex", 278 | "type": "uint256" 279 | } 280 | ], 281 | "name": "transferPunk", 282 | "outputs": [], 283 | "payable": false, 284 | "type": "function" 285 | }, 286 | { 287 | "constant": true, 288 | "inputs": [], 289 | "name": "symbol", 290 | "outputs": [ 291 | { 292 | "name": "", 293 | "type": "string" 294 | } 295 | ], 296 | "payable": false, 297 | "type": "function" 298 | }, 299 | { 300 | "constant": false, 301 | "inputs": [ 302 | { 303 | "name": "punkIndex", 304 | "type": "uint256" 305 | } 306 | ], 307 | "name": "withdrawBidForPunk", 308 | "outputs": [], 309 | "payable": false, 310 | "type": "function" 311 | }, 312 | { 313 | "constant": false, 314 | "inputs": [ 315 | { 316 | "name": "to", 317 | "type": "address" 318 | }, 319 | { 320 | "name": "punkIndex", 321 | "type": "uint256" 322 | } 323 | ], 324 | "name": "setInitialOwner", 325 | "outputs": [], 326 | "payable": false, 327 | "type": "function" 328 | }, 329 | { 330 | "constant": false, 331 | "inputs": [ 332 | { 333 | "name": "punkIndex", 334 | "type": "uint256" 335 | }, 336 | { 337 | "name": "minSalePriceInWei", 338 | "type": "uint256" 339 | }, 340 | { 341 | "name": "toAddress", 342 | "type": "address" 343 | } 344 | ], 345 | "name": "offerPunkForSaleToAddress", 346 | "outputs": [], 347 | "payable": false, 348 | "type": "function" 349 | }, 350 | { 351 | "constant": true, 352 | "inputs": [], 353 | "name": "punksRemainingToAssign", 354 | "outputs": [ 355 | { 356 | "name": "", 357 | "type": "uint256" 358 | } 359 | ], 360 | "payable": false, 361 | "type": "function" 362 | }, 363 | { 364 | "constant": false, 365 | "inputs": [ 366 | { 367 | "name": "punkIndex", 368 | "type": "uint256" 369 | }, 370 | { 371 | "name": "minSalePriceInWei", 372 | "type": "uint256" 373 | } 374 | ], 375 | "name": "offerPunkForSale", 376 | "outputs": [], 377 | "payable": false, 378 | "type": "function" 379 | }, 380 | { 381 | "constant": false, 382 | "inputs": [ 383 | { 384 | "name": "punkIndex", 385 | "type": "uint256" 386 | } 387 | ], 388 | "name": "getPunk", 389 | "outputs": [], 390 | "payable": false, 391 | "type": "function" 392 | }, 393 | { 394 | "constant": true, 395 | "inputs": [ 396 | { 397 | "name": "", 398 | "type": "address" 399 | } 400 | ], 401 | "name": "pendingWithdrawals", 402 | "outputs": [ 403 | { 404 | "name": "", 405 | "type": "uint256" 406 | } 407 | ], 408 | "payable": false, 409 | "type": "function" 410 | }, 411 | { 412 | "constant": false, 413 | "inputs": [ 414 | { 415 | "name": "punkIndex", 416 | "type": "uint256" 417 | } 418 | ], 419 | "name": "punkNoLongerForSale", 420 | "outputs": [], 421 | "payable": false, 422 | "type": "function" 423 | }, 424 | { 425 | "inputs": [], 426 | "payable": true, 427 | "type": "constructor" 428 | }, 429 | { 430 | "anonymous": false, 431 | "inputs": [ 432 | { 433 | "indexed": true, 434 | "name": "to", 435 | "type": "address" 436 | }, 437 | { 438 | "indexed": false, 439 | "name": "punkIndex", 440 | "type": "uint256" 441 | } 442 | ], 443 | "name": "Assign", 444 | "type": "event" 445 | }, 446 | { 447 | "anonymous": false, 448 | "inputs": [ 449 | { 450 | "indexed": true, 451 | "name": "from", 452 | "type": "address" 453 | }, 454 | { 455 | "indexed": true, 456 | "name": "to", 457 | "type": "address" 458 | }, 459 | { 460 | "indexed": false, 461 | "name": "value", 462 | "type": "uint256" 463 | } 464 | ], 465 | "name": "Transfer", 466 | "type": "event" 467 | }, 468 | { 469 | "anonymous": false, 470 | "inputs": [ 471 | { 472 | "indexed": true, 473 | "name": "from", 474 | "type": "address" 475 | }, 476 | { 477 | "indexed": true, 478 | "name": "to", 479 | "type": "address" 480 | }, 481 | { 482 | "indexed": false, 483 | "name": "punkIndex", 484 | "type": "uint256" 485 | } 486 | ], 487 | "name": "PunkTransfer", 488 | "type": "event" 489 | }, 490 | { 491 | "anonymous": false, 492 | "inputs": [ 493 | { 494 | "indexed": true, 495 | "name": "punkIndex", 496 | "type": "uint256" 497 | }, 498 | { 499 | "indexed": false, 500 | "name": "minValue", 501 | "type": "uint256" 502 | }, 503 | { 504 | "indexed": true, 505 | "name": "toAddress", 506 | "type": "address" 507 | } 508 | ], 509 | "name": "PunkOffered", 510 | "type": "event" 511 | }, 512 | { 513 | "anonymous": false, 514 | "inputs": [ 515 | { 516 | "indexed": true, 517 | "name": "punkIndex", 518 | "type": "uint256" 519 | }, 520 | { 521 | "indexed": false, 522 | "name": "value", 523 | "type": "uint256" 524 | }, 525 | { 526 | "indexed": true, 527 | "name": "fromAddress", 528 | "type": "address" 529 | } 530 | ], 531 | "name": "PunkBidEntered", 532 | "type": "event" 533 | }, 534 | { 535 | "anonymous": false, 536 | "inputs": [ 537 | { 538 | "indexed": true, 539 | "name": "punkIndex", 540 | "type": "uint256" 541 | }, 542 | { 543 | "indexed": false, 544 | "name": "value", 545 | "type": "uint256" 546 | }, 547 | { 548 | "indexed": true, 549 | "name": "fromAddress", 550 | "type": "address" 551 | } 552 | ], 553 | "name": "PunkBidWithdrawn", 554 | "type": "event" 555 | }, 556 | { 557 | "anonymous": false, 558 | "inputs": [ 559 | { 560 | "indexed": true, 561 | "name": "punkIndex", 562 | "type": "uint256" 563 | }, 564 | { 565 | "indexed": false, 566 | "name": "value", 567 | "type": "uint256" 568 | }, 569 | { 570 | "indexed": true, 571 | "name": "fromAddress", 572 | "type": "address" 573 | }, 574 | { 575 | "indexed": true, 576 | "name": "toAddress", 577 | "type": "address" 578 | } 579 | ], 580 | "name": "PunkBought", 581 | "type": "event" 582 | }, 583 | { 584 | "anonymous": false, 585 | "inputs": [ 586 | { 587 | "indexed": true, 588 | "name": "punkIndex", 589 | "type": "uint256" 590 | } 591 | ], 592 | "name": "PunkNoLongerForSale", 593 | "type": "event" 594 | } 595 | ] -------------------------------------------------------------------------------- /src/base.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import fetch from 'node-fetch'; 3 | import { promises as asyncfs } from 'fs'; 4 | import { HttpService } from '@nestjs/axios'; 5 | import fs from 'fs'; 6 | import fiatSymbols from './fiat-symobols.json'; 7 | import { ethers } from 'ethers'; 8 | import { catchError, defaultIfEmpty, EMPTY, firstValueFrom, map, Observable, of, switchMap, tap, timer } from 'rxjs'; 9 | import currency from 'currency.js'; 10 | 11 | import dotenv from 'dotenv'; 12 | dotenv.config(); 13 | 14 | import { config } from './config'; 15 | import TwitterClient from './clients/twitter'; 16 | import { EUploadMimeType } from 'twitter-api-v2'; 17 | import DiscordClient from './clients/discord'; 18 | import { createLogger } from './logging.utils'; 19 | import { HexColorString, MessageAttachment, MessageEmbed } from 'discord.js'; 20 | import { REST } from '@discordjs/rest'; 21 | import { Routes } from 'discord-api-types/v10'; 22 | import { formatDistance } from 'date-fns'; 23 | 24 | export const alchemyAPIUrl = 'https://eth-mainnet.alchemyapi.io/v2/'; 25 | export const alchemyAPIKey = process.env.ALCHEMY_API_KEY; 26 | 27 | //const provider = ethers.getDefaultProvider(alchemyAPIUrl + alchemyAPIKey); 28 | const provider = global.providerForceHTTPS ? 29 | ethers.getDefaultProvider(process.env.GETH_NODE_ENDPOINT_HTTP) : 30 | ethers.getDefaultProvider(process.env.GETH_NODE_ENDPOINT); 31 | 32 | 33 | const pendingTransactions = [] 34 | const MAX_PENDING_TRANSACTIONS = 20000 35 | 36 | const logger = createLogger('base.service') 37 | let pendingTransactionWatcherStarted = false 38 | 39 | let fiatValues = {} 40 | getCryptoToFiat() 41 | 42 | async function getCryptoToFiat() { 43 | logger.info('refreshing fiat values') 44 | const endpoint = `https://api.coingecko.com/api/v3/simple/price?ids=ethereum,dai,usdc&vs_currencies=usd`; 45 | const res = await fetch(endpoint) 46 | const data = await res.json() as any 47 | fiatValues = { 'usdc': { 'usd': 1 }, ...data } 48 | logger.info(`fiat values set to ${JSON.stringify(fiatValues)}`) 49 | 50 | setTimeout(() => getCryptoToFiat(), 300000) 51 | } 52 | 53 | if (!global.noWatchdog && !global.doNotStartAutomatically) { 54 | startWatchdog() 55 | } 56 | if (config.enable_flashbot_detection && !global.doNotStartAutomatically) { 57 | watchPendingTransactions() 58 | } 59 | 60 | function startWatchdog() { 61 | return setTimeout(async () => { 62 | const timeoutInterval = setTimeout(() => { 63 | logger.warn(`Websocket connection hanged! Killing myself.`) 64 | process.exit(1) 65 | }, 20000); 66 | logger.info(`Checking websocket connection...`) 67 | const block = await provider.getBlockNumber() 68 | logger.info(`Websocket connection alive: ${block} !`) 69 | clearInterval(timeoutInterval) 70 | startWatchdog() 71 | }, 30000) 72 | } 73 | 74 | function watchPendingTransactions() { 75 | if (!pendingTransactionWatcherStarted) { 76 | pendingTransactionWatcherStarted = true 77 | 78 | provider.on('pending', (txHash) => { 79 | pendingTransactions.push({ 80 | time: new Date().getTime(), 81 | hash: txHash 82 | }) 83 | }); 84 | 85 | setInterval(() => { 86 | if (pendingTransactions.length) { 87 | if (pendingTransactions.length > MAX_PENDING_TRANSACTIONS) { 88 | pendingTransactions.splice(0, pendingTransactions.length - MAX_PENDING_TRANSACTIONS) 89 | } 90 | const distanceFrom = formatDistance(pendingTransactions[0].time, new Date(), { addSuffix: true }) 91 | const distanceTo = formatDistance(pendingTransactions[pendingTransactions.length-1].time, new Date(), { addSuffix: true }) 92 | logger.info(`Analyzed ${pendingTransactions.length} pending transactions between ${distanceFrom} and ${distanceTo}...`) 93 | 94 | if (new Date().getTime() - pendingTransactions[pendingTransactions.length-1].time > 60000*5) { 95 | logger.info(`Last pending transaction is older than 5 minutes, killing myself...`) 96 | process.exit(1) 97 | } 98 | } 99 | }, 5000) 100 | } 101 | } 102 | 103 | export interface TweetRequest { 104 | platform: string, 105 | logIndex: number, 106 | eventType: string, 107 | initialFrom:string, 108 | initialTo?:string, 109 | from: any; 110 | to?: any; 111 | tokenId: string; 112 | ether?: number; 113 | erc20Token: string; 114 | transactionHash: string; 115 | transactionDate: string; 116 | alternateValue: number; 117 | imageUrl?: string; 118 | additionalText?: string; 119 | } 120 | 121 | @Injectable() 122 | export class BaseService { 123 | 124 | twitterClient: TwitterClient; 125 | discordClient: DiscordClient; 126 | 127 | constructor( 128 | protected readonly http: HttpService 129 | ) { 130 | this.twitterClient = new TwitterClient() 131 | this.discordClient = new DiscordClient() 132 | } 133 | 134 | isTransactionFlashbotted(hash:string) { 135 | if (pendingTransactions.length < MAX_PENDING_TRANSACTIONS) { 136 | logger.warn(`cannot determinate if the transaction used a flashbot because the pool is not full: ${pendingTransactions.length}`) 137 | return false 138 | } 139 | for (let tx of pendingTransactions) { 140 | if (tx.hash.toLowerCase() == hash.toLowerCase()) { 141 | return false 142 | } 143 | } 144 | return true 145 | } 146 | 147 | initDiscordClient() { 148 | this.discordClient.init() 149 | } 150 | 151 | getDiscordInteractionsListeners() { 152 | return this.discordClient.getInteractionsListener() 153 | } 154 | 155 | getDiscordCommands() { 156 | return this.discordClient.getDiscordCommands() 157 | } 158 | 159 | getWeb3Provider() { 160 | return provider 161 | } 162 | 163 | shortenAddress(address: string): string { 164 | const shortAddress = `${address.slice(0, 5)}...${address.slice(address.length - 5, address.length)}`; 165 | if (address.startsWith('0x')) return shortAddress; 166 | return address; 167 | } 168 | 169 | async getTokenMetadata(tokenId: string, onlyImage:boolean=true): Promise { 170 | // check cache 171 | const metadataPath = `${config.token_metadata_cache_path}/${tokenId}.json` 172 | const url = alchemyAPIUrl + alchemyAPIKey + '/getNFTMetadata'; 173 | const hadCacheData = fs.existsSync(metadataPath) 174 | let dataObserver = hadCacheData ? 175 | of({ 176 | data: JSON.parse(fs.readFileSync(metadataPath).toString()) 177 | }) : 178 | this.http.get(url, { 179 | params: { 180 | contractAddress: config.contract_address, 181 | tokenId, 182 | tokenType: 'erc721' 183 | } 184 | }) 185 | 186 | return await firstValueFrom( 187 | dataObserver.pipe( 188 | tap(async (res:any) => { 189 | if (!hadCacheData && config.token_metadata_cache_path) { 190 | logger.info(`populating metadata cache for ${tokenId}`) 191 | await asyncfs.writeFile(metadataPath, JSON.stringify(res?.data)) 192 | } 193 | }), 194 | map((res: any) => { 195 | return onlyImage ? res?.data?.metadata?.image_url || res?.data?.metadata?.image || res?.data?.tokenUri?.gateway : res?.data; 196 | }), 197 | catchError(() => { 198 | return of(null); 199 | }) 200 | ) 201 | ); 202 | } 203 | 204 | async dispatch(data: TweetRequest) { 205 | const tweet = await this.tweet(data) 206 | await this.discord(data, tweet.id) 207 | } 208 | 209 | async discord(data: TweetRequest, 210 | tweetId:string|undefined=undefined, 211 | template:string=config.saleMessageDiscord, 212 | color:string='#0084CA', 213 | footerTextParam:string|undefined=undefined) { 214 | if (!this.discordClient.setup) return 215 | if (tweetId) template = template.replace(new RegExp('', 'g'), ``); 216 | const image = config.use_local_images ? data.imageUrl : this.transformImage(data.imageUrl); 217 | 218 | const platformImage = data.platform === 'nftx' ? 'NFTX.png' : 219 | data.platform === 'opensea' ? 'OPENSEA.png' : 220 | data.platform === 'looksrare' ? 'LOOKSRARE.png' : 221 | data.platform === 'x2y2' ? 'X2Y2.png' : 222 | data.platform === 'rarible' ? 'RARIBLE.png' : 223 | data.platform === 'notlarvalabs' ? 'NLL.png' : 224 | data.platform === 'phunkauction' ? 'AUCTION.png' : 225 | data.platform === 'phunkflywheel' ? 'FLYWHEEL.png' : 226 | data.platform === 'blurio' ? 'BLUR.png' : 227 | 'ETHERSCAN.png'; 228 | const sentText = this.formatText(data, template) 229 | const footerText = footerTextParam ?? config.discord_footer_text 230 | const embed = new MessageEmbed() 231 | .setColor(color as HexColorString) 232 | .setImage(`attachment://token.png`) 233 | .setDescription(sentText) 234 | .setTimestamp() 235 | .setFooter({ text: footerText, iconURL: 'attachment://platform.png' }); 236 | 237 | let processedImage: Buffer | undefined; 238 | if (image) processedImage = await this.getImageFile(image); 239 | processedImage = await this.decorateImage(processedImage, data) 240 | await this.discordClient.sendEmbed(embed, processedImage, `platform_images/${platformImage}`); 241 | } 242 | 243 | async tweet(data: TweetRequest, template:string=config.saleMessage) { 244 | 245 | let tweetText = this.formatText(data, template) 246 | 247 | // Delay tweets when running live 248 | if (!global.doNotStartAutomatically) 249 | await new Promise( resolve => setTimeout(resolve, 30000) ); 250 | 251 | // Format our image to base64 252 | const image = config.use_local_images || config.use_forced_remote_image_path ? data.imageUrl : this.transformImage(data.imageUrl); 253 | 254 | let processedImage: Buffer | undefined; 255 | if (image) processedImage = await this.getImageFile(image); 256 | 257 | processedImage = await this.decorateImage(processedImage, data) 258 | 259 | let media_id: string; 260 | if (processedImage) { 261 | // Upload the item's image to Twitter & retrieve a reference to it 262 | media_id = await this.twitterClient.uploadMedia(processedImage, { 263 | mimeType: EUploadMimeType.Png, 264 | }); 265 | } 266 | 267 | // Post the tweet 👇 268 | // If you need access to this endpoint, you’ll need to apply for Elevated access via the Developer Portal. You can learn more here: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve 269 | const { data: createdTweet, errors: errors } = await this.twitterClient.tweet( 270 | tweetText, 271 | { media: { media_ids: [media_id] } }, 272 | ); 273 | if (!errors) { 274 | logger.info( 275 | `Successfully tweeted: ${createdTweet.id} -> ${createdTweet.text}`, 276 | ); 277 | return createdTweet; 278 | } else { 279 | logger.error(errors); 280 | return null; 281 | } 282 | } 283 | 284 | async decorateImage(processedImage: Buffer, data:TweetRequest): Promise { 285 | // Do nothing but can be overriden by subclasses 286 | return processedImage 287 | } 288 | 289 | formatText(data: TweetRequest, template:string) { 290 | if (!data) return template 291 | 292 | // Cash value 293 | const value = data.alternateValue && data.alternateValue > 0 ? data.alternateValue : data.ether 294 | 295 | const fiat = this.getFiatValue(value, data.erc20Token) 296 | const eth = this.getERC20Value(data.alternateValue ? data.alternateValue : data.ether, data.erc20Token) 297 | 298 | // Replace tokens from config file 299 | template = template.replace(new RegExp('', 'g'), data.tokenId); 300 | template = template.replace(new RegExp('', 'g'), eth.format()); 301 | template = template.replace(new RegExp('', 'g'), data.transactionHash); 302 | template = template.replace(new RegExp('', 'g'), data.from); 303 | template = template.replace(new RegExp('', 'g'), data.initialFrom); 304 | template = template.replace(new RegExp('', 'g'), data.to); 305 | template = template.replace(new RegExp('', 'g'), data.initialTo); 306 | template = template.replace(new RegExp('', 'g'), fiat ? fiat.format() : '-'); 307 | const platform = data.platform === 'blurio' ? 'Blur marketplace' : 308 | data.platform === 'opensea' ? 'OpenSea marketplace' : 309 | data.platform === 'looksrare' ? 'Looks Rare' : 310 | data.platform === 'arcadexyz' ? 'Arcade' : 311 | data.platform === 'nftfi' ? 'NFTfi' : 312 | data.platform === 'benddao' ? 'Bend DAO' : 313 | data.platform === 'metastreet' ? 'Metastreet' : 314 | data.platform === 'punksmarketplace' ? 'CryptoPunks marketplace' : 315 | data.platform 316 | template = template.replace(new RegExp('', 'g'), platform); 317 | template = template.replace(new RegExp('', 'g'), data.additionalText); 318 | 319 | 320 | if (config.enable_flashbot_detection && data.eventType !== 'loans') 321 | template += ` — Flashbots Protect RPC: ${this.isTransactionFlashbotted(data.transactionHash) ? 'Yes' : 'No'}` 322 | 323 | return template 324 | } 325 | 326 | getERC20Value(value: number, erc20Token: string, forcedSymbol:string|undefined=undefined) { 327 | const symbol = forcedSymbol !== undefined ? forcedSymbol 328 | : erc20Token === 'dai' ? 'DAI' 329 | : erc20Token === 'usdc' ? 'USDC' : 'Ξ' 330 | const precision = erc20Token === 'dai' ? 0 : erc20Token === 'usdc' ? 2 : 3 331 | const pattern = erc20Token === 'dai' ? '# !' : erc20Token === 'usdc' ? '# !' : '!#' 332 | const eth = currency(value, { symbol, precision, pattern }); 333 | return eth 334 | } 335 | 336 | getFiatValue(value: number, erc20Token: string) { 337 | try { 338 | const fiatValue = fiatValues && Object.values(fiatValues).length ? 339 | fiatValues[erc20Token][config.currency] * value : 340 | undefined; 341 | return fiatValue != null ? currency(fiatValue, { symbol: fiatSymbols[config.currency].symbol, precision: 0 }) : undefined 342 | } catch (err) { 343 | logger.error(`cannot get fiat for ${erc20Token}`) 344 | } 345 | return undefined 346 | } 347 | 348 | async getImageFile(url: string): Promise { 349 | return new Promise((resolve, _) => { 350 | if (url.startsWith('http')) { 351 | this.http.get(url, { responseType: 'arraybuffer' }).subscribe((res) => { 352 | if (res.data) { 353 | const file = Buffer.from(res.data, 'binary'); 354 | resolve(file); 355 | } else { 356 | resolve(undefined); 357 | } 358 | }); 359 | } else { 360 | resolve(fs.readFileSync(url)); 361 | } 362 | }); 363 | } 364 | 365 | getCryptoToFiat(): Observable { 366 | const endpoint = `https://api.coingecko.com/api/v3/simple/price`; 367 | const params = { 368 | ids: 'ethereum,dai,usdc', 369 | vs_currencies: 'usd' 370 | }; 371 | return timer(0, 300000).pipe( 372 | switchMap(() => this.http.get(endpoint, {params})), 373 | map((res: any) => res.data), 374 | // tap((res) => console.log(res)), 375 | catchError((err: any) => { 376 | logger.warn('coin gecko call failed, ignoring fiat price', err.toString()); 377 | return of(undefined); 378 | }) 379 | ); 380 | } 381 | 382 | transformImage(value: string): string { 383 | //return value.replace('https://gateway.pinata.cloud/ipfs/QmSv6qnW1zCqiYBHCJKbfBu8YAcJefUYtPsDea3TsG2PHz/notpunk', 'file://./token_images/phunk'); 384 | let val: any = value; 385 | if (value?.includes('gateway.pinata.cloud')) { 386 | val = value.replace('gateway.pinata.cloud', 'cloudflare-ipfs.com'); 387 | // } else if (value?.startsWith('data:image')) { 388 | // val = `${value}`; 389 | } else if (value?.startsWith('ipfs://')) { 390 | val = value.replace('ipfs://', 'https://cloudflare-ipfs.com/ipfs/'); 391 | } 392 | return val ? val : null; 393 | } 394 | 395 | async updatePosition(position) { 396 | await asyncfs.writeFile(this.getPositionFile(), `${position}`) 397 | } 398 | 399 | getPositionFile():string { 400 | throw new Error('must be overriden') 401 | } 402 | 403 | } 404 | 405 | -------------------------------------------------------------------------------- /src/abi/phunkAuctionHouse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "uint256", 8 | "name": "phunkId", 9 | "type": "uint256" 10 | }, 11 | { 12 | "indexed": false, 13 | "internalType": "uint256", 14 | "name": "auctionId", 15 | "type": "uint256" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "address", 20 | "name": "sender", 21 | "type": "address" 22 | }, 23 | { 24 | "indexed": false, 25 | "internalType": "uint256", 26 | "name": "value", 27 | "type": "uint256" 28 | }, 29 | { 30 | "indexed": false, 31 | "internalType": "bool", 32 | "name": "extended", 33 | "type": "bool" 34 | } 35 | ], 36 | "name": "AuctionBid", 37 | "type": "event" 38 | }, 39 | { 40 | "anonymous": false, 41 | "inputs": [ 42 | { 43 | "indexed": true, 44 | "internalType": "uint256", 45 | "name": "phunkId", 46 | "type": "uint256" 47 | }, 48 | { 49 | "indexed": false, 50 | "internalType": "uint256", 51 | "name": "auctionId", 52 | "type": "uint256" 53 | }, 54 | { 55 | "indexed": false, 56 | "internalType": "uint256", 57 | "name": "startTime", 58 | "type": "uint256" 59 | }, 60 | { 61 | "indexed": false, 62 | "internalType": "uint256", 63 | "name": "endTime", 64 | "type": "uint256" 65 | } 66 | ], 67 | "name": "AuctionCreated", 68 | "type": "event" 69 | }, 70 | { 71 | "anonymous": false, 72 | "inputs": [ 73 | { 74 | "indexed": false, 75 | "internalType": "uint256", 76 | "name": "duration", 77 | "type": "uint256" 78 | } 79 | ], 80 | "name": "AuctionDurationUpdated", 81 | "type": "event" 82 | }, 83 | { 84 | "anonymous": false, 85 | "inputs": [ 86 | { 87 | "indexed": true, 88 | "internalType": "uint256", 89 | "name": "phunkId", 90 | "type": "uint256" 91 | }, 92 | { 93 | "indexed": false, 94 | "internalType": "uint256", 95 | "name": "auctionId", 96 | "type": "uint256" 97 | }, 98 | { 99 | "indexed": false, 100 | "internalType": "uint256", 101 | "name": "endTime", 102 | "type": "uint256" 103 | } 104 | ], 105 | "name": "AuctionExtended", 106 | "type": "event" 107 | }, 108 | { 109 | "anonymous": false, 110 | "inputs": [ 111 | { 112 | "indexed": false, 113 | "internalType": "uint256", 114 | "name": "minBidIncrementPercentage", 115 | "type": "uint256" 116 | } 117 | ], 118 | "name": "AuctionMinBidIncrementPercentageUpdated", 119 | "type": "event" 120 | }, 121 | { 122 | "anonymous": false, 123 | "inputs": [ 124 | { 125 | "indexed": false, 126 | "internalType": "uint256", 127 | "name": "reservePrice", 128 | "type": "uint256" 129 | } 130 | ], 131 | "name": "AuctionReservePriceUpdated", 132 | "type": "event" 133 | }, 134 | { 135 | "anonymous": false, 136 | "inputs": [ 137 | { 138 | "indexed": true, 139 | "internalType": "uint256", 140 | "name": "phunkId", 141 | "type": "uint256" 142 | }, 143 | { 144 | "indexed": false, 145 | "internalType": "uint256", 146 | "name": "auctionId", 147 | "type": "uint256" 148 | }, 149 | { 150 | "indexed": false, 151 | "internalType": "address", 152 | "name": "winner", 153 | "type": "address" 154 | }, 155 | { 156 | "indexed": false, 157 | "internalType": "uint256", 158 | "name": "amount", 159 | "type": "uint256" 160 | } 161 | ], 162 | "name": "AuctionSettled", 163 | "type": "event" 164 | }, 165 | { 166 | "anonymous": false, 167 | "inputs": [ 168 | { 169 | "indexed": false, 170 | "internalType": "uint256", 171 | "name": "timeBuffer", 172 | "type": "uint256" 173 | } 174 | ], 175 | "name": "AuctionTimeBufferUpdated", 176 | "type": "event" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "internalType": "address", 184 | "name": "previousOwner", 185 | "type": "address" 186 | }, 187 | { 188 | "indexed": true, 189 | "internalType": "address", 190 | "name": "newOwner", 191 | "type": "address" 192 | } 193 | ], 194 | "name": "OwnershipTransferred", 195 | "type": "event" 196 | }, 197 | { 198 | "anonymous": false, 199 | "inputs": [ 200 | { 201 | "indexed": false, 202 | "internalType": "address", 203 | "name": "account", 204 | "type": "address" 205 | } 206 | ], 207 | "name": "Paused", 208 | "type": "event" 209 | }, 210 | { 211 | "anonymous": false, 212 | "inputs": [ 213 | { 214 | "indexed": false, 215 | "internalType": "address", 216 | "name": "account", 217 | "type": "address" 218 | } 219 | ], 220 | "name": "Unpaused", 221 | "type": "event" 222 | }, 223 | { 224 | "inputs": [], 225 | "name": "auction", 226 | "outputs": [ 227 | { 228 | "internalType": "uint256", 229 | "name": "phunkId", 230 | "type": "uint256" 231 | }, 232 | { 233 | "internalType": "uint256", 234 | "name": "amount", 235 | "type": "uint256" 236 | }, 237 | { 238 | "internalType": "uint256", 239 | "name": "startTime", 240 | "type": "uint256" 241 | }, 242 | { 243 | "internalType": "uint256", 244 | "name": "endTime", 245 | "type": "uint256" 246 | }, 247 | { 248 | "internalType": "address payable", 249 | "name": "bidder", 250 | "type": "address" 251 | }, 252 | { 253 | "internalType": "bool", 254 | "name": "settled", 255 | "type": "bool" 256 | }, 257 | { 258 | "internalType": "uint256", 259 | "name": "auctionId", 260 | "type": "uint256" 261 | } 262 | ], 263 | "stateMutability": "view", 264 | "type": "function" 265 | }, 266 | { 267 | "inputs": [], 268 | "name": "auctionId", 269 | "outputs": [ 270 | { 271 | "internalType": "uint256", 272 | "name": "", 273 | "type": "uint256" 274 | } 275 | ], 276 | "stateMutability": "view", 277 | "type": "function" 278 | }, 279 | { 280 | "inputs": [ 281 | { 282 | "internalType": "uint256", 283 | "name": "phunkId", 284 | "type": "uint256" 285 | } 286 | ], 287 | "name": "createBid", 288 | "outputs": [], 289 | "stateMutability": "payable", 290 | "type": "function" 291 | }, 292 | { 293 | "inputs": [ 294 | { 295 | "internalType": "uint256", 296 | "name": "_phunkId", 297 | "type": "uint256" 298 | }, 299 | { 300 | "internalType": "uint256", 301 | "name": "_endTime", 302 | "type": "uint256" 303 | } 304 | ], 305 | "name": "createSpecialAuction", 306 | "outputs": [], 307 | "stateMutability": "nonpayable", 308 | "type": "function" 309 | }, 310 | { 311 | "inputs": [], 312 | "name": "duration", 313 | "outputs": [ 314 | { 315 | "internalType": "uint256", 316 | "name": "", 317 | "type": "uint256" 318 | } 319 | ], 320 | "stateMutability": "view", 321 | "type": "function" 322 | }, 323 | { 324 | "inputs": [ 325 | { 326 | "internalType": "contract IPhunksToken", 327 | "name": "_phunks", 328 | "type": "address" 329 | }, 330 | { 331 | "internalType": "address", 332 | "name": "_weth", 333 | "type": "address" 334 | }, 335 | { 336 | "internalType": "uint256", 337 | "name": "_timeBuffer", 338 | "type": "uint256" 339 | }, 340 | { 341 | "internalType": "uint256", 342 | "name": "_reservePrice", 343 | "type": "uint256" 344 | }, 345 | { 346 | "internalType": "uint8", 347 | "name": "_minBidIncrementPercentage", 348 | "type": "uint8" 349 | }, 350 | { 351 | "internalType": "uint256", 352 | "name": "_duration", 353 | "type": "uint256" 354 | }, 355 | { 356 | "internalType": "address", 357 | "name": "_treasuryWallet", 358 | "type": "address" 359 | } 360 | ], 361 | "name": "initialize", 362 | "outputs": [], 363 | "stateMutability": "nonpayable", 364 | "type": "function" 365 | }, 366 | { 367 | "inputs": [], 368 | "name": "minBidIncrementPercentage", 369 | "outputs": [ 370 | { 371 | "internalType": "uint8", 372 | "name": "", 373 | "type": "uint8" 374 | } 375 | ], 376 | "stateMutability": "view", 377 | "type": "function" 378 | }, 379 | { 380 | "inputs": [], 381 | "name": "owner", 382 | "outputs": [ 383 | { 384 | "internalType": "address", 385 | "name": "", 386 | "type": "address" 387 | } 388 | ], 389 | "stateMutability": "view", 390 | "type": "function" 391 | }, 392 | { 393 | "inputs": [], 394 | "name": "pause", 395 | "outputs": [], 396 | "stateMutability": "nonpayable", 397 | "type": "function" 398 | }, 399 | { 400 | "inputs": [], 401 | "name": "paused", 402 | "outputs": [ 403 | { 404 | "internalType": "bool", 405 | "name": "", 406 | "type": "bool" 407 | } 408 | ], 409 | "stateMutability": "view", 410 | "type": "function" 411 | }, 412 | { 413 | "inputs": [], 414 | "name": "phunks", 415 | "outputs": [ 416 | { 417 | "internalType": "contract IPhunksToken", 418 | "name": "", 419 | "type": "address" 420 | } 421 | ], 422 | "stateMutability": "view", 423 | "type": "function" 424 | }, 425 | { 426 | "inputs": [], 427 | "name": "renounceOwnership", 428 | "outputs": [], 429 | "stateMutability": "nonpayable", 430 | "type": "function" 431 | }, 432 | { 433 | "inputs": [], 434 | "name": "reservePrice", 435 | "outputs": [ 436 | { 437 | "internalType": "uint256", 438 | "name": "", 439 | "type": "uint256" 440 | } 441 | ], 442 | "stateMutability": "view", 443 | "type": "function" 444 | }, 445 | { 446 | "inputs": [ 447 | { 448 | "internalType": "uint256", 449 | "name": "_duration", 450 | "type": "uint256" 451 | } 452 | ], 453 | "name": "setDuration", 454 | "outputs": [], 455 | "stateMutability": "nonpayable", 456 | "type": "function" 457 | }, 458 | { 459 | "inputs": [ 460 | { 461 | "internalType": "uint8", 462 | "name": "_minBidIncrementPercentage", 463 | "type": "uint8" 464 | } 465 | ], 466 | "name": "setMinBidIncrementPercentage", 467 | "outputs": [], 468 | "stateMutability": "nonpayable", 469 | "type": "function" 470 | }, 471 | { 472 | "inputs": [ 473 | { 474 | "internalType": "uint256", 475 | "name": "_reservePrice", 476 | "type": "uint256" 477 | } 478 | ], 479 | "name": "setReservePrice", 480 | "outputs": [], 481 | "stateMutability": "nonpayable", 482 | "type": "function" 483 | }, 484 | { 485 | "inputs": [ 486 | { 487 | "internalType": "uint256", 488 | "name": "_timeBuffer", 489 | "type": "uint256" 490 | } 491 | ], 492 | "name": "setTimeBuffer", 493 | "outputs": [], 494 | "stateMutability": "nonpayable", 495 | "type": "function" 496 | }, 497 | { 498 | "inputs": [ 499 | { 500 | "internalType": "address", 501 | "name": "_treasuryWallet", 502 | "type": "address" 503 | } 504 | ], 505 | "name": "setTreasuryWallet", 506 | "outputs": [], 507 | "stateMutability": "nonpayable", 508 | "type": "function" 509 | }, 510 | { 511 | "inputs": [], 512 | "name": "settleAuction", 513 | "outputs": [], 514 | "stateMutability": "nonpayable", 515 | "type": "function" 516 | }, 517 | { 518 | "inputs": [], 519 | "name": "settleCurrentAndCreateNewAuction", 520 | "outputs": [], 521 | "stateMutability": "nonpayable", 522 | "type": "function" 523 | }, 524 | { 525 | "inputs": [], 526 | "name": "timeBuffer", 527 | "outputs": [ 528 | { 529 | "internalType": "uint256", 530 | "name": "", 531 | "type": "uint256" 532 | } 533 | ], 534 | "stateMutability": "view", 535 | "type": "function" 536 | }, 537 | { 538 | "inputs": [ 539 | { 540 | "internalType": "address", 541 | "name": "newOwner", 542 | "type": "address" 543 | } 544 | ], 545 | "name": "transferOwnership", 546 | "outputs": [], 547 | "stateMutability": "nonpayable", 548 | "type": "function" 549 | }, 550 | { 551 | "inputs": [], 552 | "name": "treasuryWallet", 553 | "outputs": [ 554 | { 555 | "internalType": "address", 556 | "name": "", 557 | "type": "address" 558 | } 559 | ], 560 | "stateMutability": "view", 561 | "type": "function" 562 | }, 563 | { 564 | "inputs": [], 565 | "name": "unpause", 566 | "outputs": [], 567 | "stateMutability": "nonpayable", 568 | "type": "function" 569 | }, 570 | { 571 | "inputs": [], 572 | "name": "weth", 573 | "outputs": [ 574 | { 575 | "internalType": "address", 576 | "name": "", 577 | "type": "address" 578 | } 579 | ], 580 | "stateMutability": "view", 581 | "type": "function" 582 | } 583 | ] --------------------------------------------------------------------------------