├── .gitignore ├── package.json ├── sample.env ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | *.sw* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nfttweet", 3 | "version": "1.0.0", 4 | "description": "Tweet new NFT tokens", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^0.21.1", 14 | "dotenv": "^8.2.0", 15 | "node-rest-client-promise": "^3.1.1", 16 | "twitter": "^1.7.1", 17 | "web3": "^1.3.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | TWITTER_API_KEY=YOUR-API-KEY 2 | TWITTER_API_SECRET_KEY=YOUR-API-SECRET 3 | TWITTER_ACCESS_TOKEN=YOUR-ACCESS-TOKEN 4 | TWITTER_ACCESS_TOKEN_SECRET=YOUR-TOKEN-SECRET 5 | TWITTER_MESSAGE_TEMPLATE="${event.event} event heard at transaction hash ${event.transactionHash}, Block number: ${event.blockNumber}" 6 | WSURL=wss://mainnet.infura.io/ws/v3/YOUR-PROJECT-ID 7 | ETHERSCAN_ABI_URL=https://api-YOUR-ACCOUNT.etherscan.io/api?module=contract&action=getabi&address= 8 | ETHERSCAN_API_KEY=YOUR-API-KEY 9 | CONTRACT_ADDRESS=0xffffffffffffffffffffffffffffffffffffffff 10 | CONTRACT_EVENTS=Deposit,Withdrawal 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NFT Tweet 2 | 3 | Monitors an NFT contract for `Mint` events 4 | 5 | ## Setup 6 | 7 | After configuring, and installing dependencies run `npm run start` to start the service 8 | 9 | ### Dependencies 10 | 11 | * Use node version 12 12 | * run `npm install` 13 | 14 | ### Config 15 | 16 | * Copy `sample.env` to `.env` 17 | * Generate the appropriate credentials for twitter, etherscan, and infura 18 | * Populate the `.env` file with your credentials 19 | 20 | ### Twitter message 21 | 22 | Use the `TWITTER_MESSAGE_TEMPLATE` value in `.env` to format your tweet. 23 | You will have access to the `event` object (example below). 24 | The value will be interpreted as a string literal and may include variables of format `${event.transactionHash}` 25 | 26 | ```javascript 27 | { 28 | removed: false, 29 | logIndex: 342, 30 | transactionIndex: 294, 31 | transactionHash: '0x48378b555048baf27aed8fc7f4e1526a64dd91a2206d0d79690ee77e063ce97e', 32 | blockHash: '0xd0d3af59b2eca4bff3651ee09417f105756d206a4dd84674893f64fd31bf9dbe', 33 | blockNumber: 12310282, 34 | address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 35 | id: 'log_8626df84', 36 | returnValues: Result { 37 | '0': '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 38 | '1': '6610245104149876', 39 | src: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 40 | wad: '6610245104149876' 41 | }, 42 | event: 'Withdrawal', 43 | signature: '0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65', 44 | raw: { 45 | data: '0x00000000000000000000000000000000000000000000000000177bfb9f4fa574', 46 | topics: [ 47 | '0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65' 48 | '0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d' 49 | ] 50 | } 51 | } 52 | ``` 53 | 54 | ## Running the app 55 | 56 | * Use node version 12 (I suggest using [nvm](https://github.com/nvm-sh/nvm)) 57 | * Run the app using node: 58 | ``` 59 | node index.js 60 | ``` 61 | * You might want to use a tool like [PM2](https://pm2.keymetrics.io/) 62 | * Good luck! 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('dotenv').config() 4 | 5 | const { 6 | TWITTER_API_KEY, 7 | TWITTER_API_SECRET_KEY, 8 | TWITTER_ACCESS_TOKEN, 9 | TWITTER_ACCESS_TOKEN_SECRET, 10 | TWITTER_MESSAGE_TEMPLATE, 11 | WSURL, 12 | ETHERSCAN_ABI_URL, 13 | ETHERSCAN_API_KEY, 14 | CONTRACT_ADDRESS, 15 | CONTRACT_EVENTS } = process.env 16 | 17 | const CONTRACT_EVENTS_ARRAY = CONTRACT_EVENTS.split(',') 18 | const Web3 = require('web3') 19 | const Twitter = require('twitter') 20 | const restClient = require('node-rest-client-promise').Client() 21 | const axios = require('axios') 22 | const web3 = new Web3(new Web3.providers.WebsocketProvider(WSURL)) 23 | 24 | const twitterClient = new Twitter({ 25 | consumer_key: TWITTER_API_KEY, 26 | consumer_secret: TWITTER_API_SECRET_KEY, 27 | access_token_key: TWITTER_ACCESS_TOKEN, 28 | access_token_secret: TWITTER_ACCESS_TOKEN_SECRET 29 | }) 30 | 31 | async function postToTwitter(event) { 32 | const msg = eval('`'+ TWITTER_MESSAGE_TEMPLATE + '`') // can replace this with message template 33 | let opts = { status: msg } 34 | 35 | // If you always use an image you can use `if (true)` here or remove conditional 36 | if (TWITTER_MESSAGE_TEMPLATE.indexOf('metadata.data.image') >= 0) { 37 | const img_url = event.metadata.data.image 38 | 39 | // get png binary data 40 | const result = await axios.request({ 41 | responseType: 'arraybuffer', 42 | url: img_url, 43 | method: 'get', 44 | headers: { 45 | 'Content-Type': 'image/png', // if the image type changes this too 46 | }, 47 | }) 48 | 49 | const data = result.data 50 | const uploadResult = await twitterClient.post('media/upload', { media: data}) 51 | const media_id = uploadResult.media_id 52 | opts = { status: msg, media_ids: media_id } 53 | } 54 | 55 | return twitterClient.post('statuses/update', opts, function(error, tweet, response) { 56 | if (error) return console.log(JSON.stringify(error)) 57 | }) 58 | } 59 | 60 | async function getContractAbi() { 61 | const url = `${ETHERSCAN_ABI_URL}${CONTRACT_ADDRESS}&apiKey=${ETHERSCAN_API_KEY}` 62 | const etherescan_response = await restClient.getPromise(url) 63 | const contract_abi = JSON.parse(etherescan_response.data.result) 64 | return contract_abi 65 | } 66 | 67 | async function eventQuery(){ 68 | const contract_abi = await getContractAbi() 69 | const contract = new web3.eth.Contract(contract_abi, CONTRACT_ADDRESS) 70 | let lastHash 71 | contract.events.allEvents() 72 | .on('data', (event) => { 73 | if (CONTRACT_EVENTS_ARRAY.includes(event.event) && event.transactionHash !== lastHash) { 74 | lastHash = event.transactionHash // dedupe 75 | postToTwitter(event) 76 | } 77 | }) 78 | .on('error', console.error) 79 | } 80 | 81 | eventQuery() 82 | --------------------------------------------------------------------------------