├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── lib └── chainsaw.js ├── package.json └── tests ├── .eslintrc ├── chainsaw.js ├── config.json ├── contracts └── StubPaymentChannel.sol ├── loader_test.js ├── setup.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["*.min.js"], 3 | "compact": false, 4 | "presets": [["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }], "stage-2"] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: standard 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Debug log from npm 30 | npm-debug.log 31 | 32 | # Package lock 33 | package-lock.json 34 | 35 | # sass 36 | .sass-cache/ 37 | *.css.map 38 | 39 | #ngrok 40 | ngrok 41 | 42 | #output 43 | output 44 | 45 | #examples 46 | examples 47 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build 2 | lib 3 | js 4 | utils 5 | test_contracts 6 | tests 7 | buyer_adderess.txt 8 | examples 9 | loader_test.js 10 | output 11 | node_modules 12 | distribution 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chainsaw 2 | 3 | Chainsaw is ethereum based log extracting and log decoding library with a periodic polling feature. 4 | 5 | ## Usage of Chainsaw . 6 | 7 | ### 1. Build Chainsaw : 8 | 9 | Run the below command in the chainsaw directory . 10 | 11 | ``` 12 | npm install eth-chainsaw 13 | ``` 14 | 15 | ### 2. Importing the chainsaw : 16 | 17 | For non babel transpiled es6 : 18 | 19 | ```javascript 20 | const Chainsaw = require('eth-chainsaw').Chainsaw 21 | ``` 22 | 23 | if your server is a es6 babel transpiled file , import in the following way: 24 | 25 | ```javascript 26 | import { Chainsaw } from 'eth-chainsaw' 27 | ``` 28 | 29 | ### 3. Instantiating chainsaw, initializing with web3 provider and deployed contract address : 30 | 31 | ```javascript 32 | const Web3 = require('web3') 33 | const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 34 | // web3 , list of contract address 35 | const chainsaw = new Chainsaw(web3, [List of contract address]) 36 | 37 | // Add abi of your contracts to chainsaw,so chainsaw is able to decode the logs. 38 | chainsaw.addABI(testContract.abi) 39 | ``` 40 | 41 | ### 4. Event callback and Turn On Chainsaw Polling : 42 | 43 | _Define event callback_ . 44 | 45 | ```javascript 46 | // Chainsaw event callback functions 47 | const eventCallBack = (error, eventData) => { 48 | if (!error && eventData.length > 0) { 49 | console.log('Chainsaw eventCallBack', eventData) 50 | } 51 | } 52 | ``` 53 | 54 | _Turn on Polling in the following way_ : 55 | 56 | ```javascript 57 | // Chainsaw turn on polling to listen to events 58 | chainsaw.turnOnPolling(eventCallBack) 59 | ``` 60 | 61 | ### 6. Complete Working Example of Usage : 62 | 63 | ```javascript 64 | // Importing Chainsaw 65 | const Chainsaw = require('eth-chainsaw').Chainsaw 66 | [a relative link] (./tests/setup.js) 67 | const setup = require('./tests/setup.js') 68 | 69 | const Web3 = require('web3') 70 | const app = require('express')() 71 | 72 | const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 73 | 74 | // Chainsaw event callback functions 75 | const eventCallBack = (error, eventData) => { 76 | if (!error && eventData.length > 0) { 77 | console.log('Chainsaw eventCallBack', eventData) 78 | } 79 | } 80 | 81 | const initChainsaw = async () => { 82 | // Following deploys a test contract. Its responsibility 83 | // of the client of chainsaw to deploy their respective contracts. 84 | // Chainsaw does not have configs , it only has initializaiton 85 | // parameters when you instantiate a class . 86 | const testContract = await setup.default({ 87 | testRPCProvider: 'http://localhost:8545/' 88 | }) 89 | 90 | // Initialize with web3 provider and contract address to watch. 91 | const chainsaw = new Chainsaw(web3, [testContract.address]) 92 | 93 | // Add abi of the contract to chainsaw. 94 | chainsaw.addABI(testContract.abi) 95 | 96 | // Chainsaw turn on polling to listen to events 97 | chainsaw.turnOnPolling(eventCallBack) 98 | 99 | // Now call your contract methods to receive event data 100 | // in the callback. Following is just a test example 101 | await testContract.deposit('0xabc', {value: 20} ) 102 | } 103 | 104 | initChainsaw() 105 | 106 | app.listen(3000, function () { 107 | console.log('Chainsaw Example Usage ') 108 | }) 109 | ``` 110 | 111 | ## Other Implemented Methods: 112 | 113 | ### Get undecoded logs by block number: 114 | 115 | _Function_: 116 | 117 | ```javascript 118 | /** 119 | ** Given the blocknumber return the array of logs for each transaction. 120 | ** blockNumber -> Block: [ txHash1, txHash2] -> Logs: [logs1, logs2] 121 | **/ 122 | getLogsByBlockNumber (blockNumber) 123 | ``` 124 | 125 | _Example Usage_ : 126 | 127 | ```javascript 128 | chainsaw.getLogsByBlockNumber(web3.eth.blockNumber) 129 | ``` 130 | 131 | ### Get decoded logs by block range: 132 | 133 | _Function_: 134 | 135 | ```javascript 136 | /** 137 | ** Given an startBlock and endBlock range, decoded logs are returned. 138 | ** Params - 139 | ** startBlock: Starting block to read the block. (default: latest block) 140 | ** endBlock: End block to read the block.(default: latest block) 141 | **/ 142 | getLogs (startBlock = this.eth.blockNumber, endBlock = this.eth.blockNumber) 143 | ``` 144 | 145 | _Example Usage_ : Reads from block 100 to latestBlock . 146 | 147 | ```javascript 148 | chainsaw.getLogs(100, web3.eth.blockNumber) 149 | ``` 150 | 151 | ### Chainsaw event object format 152 | 153 | ```json 154 | { 155 | "name": "DidCreateChannel", 156 | "events": 157 | [ { "name": "viewer", 158 | "type": "address", 159 | "value": "0x50f485d16569013b785524c8d96720cee14fcf8b" }, 160 | { "name": "broadcaster", 161 | "type": "address", 162 | "value": "0x488767fdbd05d7c516357df8a6495171c20f2d81" }, 163 | { "name": "channelId", 164 | "type": "bytes32", 165 | "value": "0x2223420000000000000000000000000000000000000000000000000000000000" } ], 166 | "address": "0x454671f51b892b1597488235785279a1bcb42600", 167 | "logIndex": 0, 168 | "blockHash": "0xe1ff93d04753a5750ba0827de2d8067b21b8fbe47ff10cd3560a5e98b7ea67e7", 169 | "blockNumber": 98, 170 | "contractAddress": "0x454671f51b892b1597488235785279a1bcb42600", 171 | "sender": "0x50f485d16569013b785524c8d96720cee14fcf8b", 172 | "receiver": "0x454671f51b892b1597488235785279a1bcb42600", 173 | "ts": 1506492156 174 | } 175 | ``` 176 | 177 | ### Running chainsaw tests 178 | 179 | ``` 180 | npm test 181 | ``` 182 | 183 | Please make sure to edit config.json with web3 http provider of your choice . If you do use anything other than 184 | testrpc for running tests , please make sure that first 10 accounts `web3.eth.accounts` are unlocked and has some ether to cover the gas cost for calling contract methods . 185 | 186 | ```json 187 | { 188 | "WEB3_PROVIDER": "http://localhost:8545" 189 | } 190 | ``` 191 | -------------------------------------------------------------------------------- /lib/chainsaw.js: -------------------------------------------------------------------------------- 1 | import p from 'es6-promisify' 2 | const abiDecoder = require('abi-decoder') 3 | 4 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) 5 | 6 | // Lives inside a long-running process 7 | // Usage patterns: 8 | // 1. Polling - given a set of contract addresses, emit all events logged for that contract 9 | // 2. Querying - given a block range and an address, return all events logged 10 | 11 | /** 12 | ** Chainsaw is a ethereum log extractor and log decoding library. 13 | **/ 14 | class Chainsaw { 15 | constructor (web3, contractAddresses = [], pollingInterval = 1000) { 16 | this.web3 = web3 17 | this.eth = web3.eth 18 | this.contractAddresses = contractAddresses 19 | this.isPolling = false 20 | this.lastReadBlockNumber = this.eth.blockNumber 21 | this.generator = null 22 | this.pollingInterval = pollingInterval 23 | } 24 | 25 | /** 26 | ** Given the blocknumber return the array of logs for each transaction. 27 | ** blockNumber -> Block: [ txHash1, txHash2] -> Logs: [logs1, logs2] 28 | **/ 29 | getLogsByBlockNumber (blockNumber) { 30 | if (blockNumber < 0 || blockNumber > this.eth.blockNumber) { 31 | throw new Error('Invalid blockNumber : blockNumber should be greater than zero and less than latestBlock.') 32 | } 33 | 34 | let block = this.eth.getBlock(blockNumber) 35 | if (block && block['hash']) { 36 | let transactionHashes = block['transactions'] 37 | return transactionHashes.map(tx => { 38 | // We need to get the transactions sender and receiver 39 | let transactionData = this.eth.getTransaction(tx) 40 | // If no logs for a transaction it is omitted 41 | let receipt = this.eth.getTransactionReceipt(tx) 42 | if (receipt && receipt['logs']) { 43 | receipt['logs']['timestamp'] = block['timestamp'] 44 | receipt['logs']['sender'] = transactionData['from'] 45 | receipt['logs']['receiver'] = transactionData['to'] 46 | return receipt['logs'] 47 | } 48 | }).filter(a => a.length > 0) 49 | } else { 50 | return [] 51 | } 52 | } 53 | 54 | /** 55 | ** Add abi for decoder before being able to decode logs 56 | ** in the block . 57 | **/ 58 | addABI (abi) { 59 | abiDecoder.addABI(abi) 60 | } 61 | 62 | /* 63 | ** Utility function to construct 64 | ** decoded logs to return 65 | **/ 66 | constructLogs (dLog, i, transactionIndex, decodedLogs, logsInTheBlock) { 67 | dLog['logIndex'] = decodedLogs[i]['logIndex'] 68 | dLog['blockHash'] = decodedLogs[i]['blockHash'] 69 | dLog['blockNumber'] = decodedLogs[i]['blockNumber'] 70 | dLog['contractAddress'] = decodedLogs[i]['address'] 71 | dLog['sender'] = logsInTheBlock[transactionIndex]['sender'] 72 | dLog['receiver'] = logsInTheBlock[transactionIndex]['receiver'] 73 | dLog['eventType'] = dLog['name'] 74 | dLog['fields'] = dLog['events'] 75 | dLog['ts'] = logsInTheBlock[transactionIndex]['timestamp'] 76 | return dLog 77 | } 78 | /** 79 | ** Given an startBlock and endBlock range, decoded logs are returned. 80 | ** Params - 81 | ** startBlock: Starting block to read the block. (default: latest block) 82 | ** endBlock: End block to read the block.(default: latest block) 83 | **/ 84 | getLogs (startBlock = this.eth.blockNumber, endBlock = this.eth.blockNumber) { 85 | if (startBlock > this.eth.blockNumer || startBlock < 0) { 86 | throw new Error('Invalid startBlock: Must be below web3.eth.blockNumber or startBlock cannot be below 0') 87 | } 88 | 89 | if (startBlock > endBlock) { 90 | throw new Error('Invalid startBlock: Must be below endBlock') 91 | } 92 | 93 | if (endBlock > this.eth.blockNumber) { 94 | throw new Error('Invalid endBlock: Must be less than or equal to latest block') 95 | } 96 | 97 | let logs = [] 98 | for (let i = startBlock; i <= endBlock; i++) { 99 | const logsInTheBlock = this.getLogsByBlockNumber(i) 100 | let transactionIndex = 0 101 | logs.push(logsInTheBlock.map(log => { 102 | log = log.filter(a => this.contractAddresses.indexOf(a.address) >= 0) 103 | let decodedLogs = abiDecoder.decodeLogs(log) 104 | // Formating decoded logs to add extra data needed. 105 | decodedLogs = decodedLogs.map((dLog, i) => { 106 | return this.constructLogs(dLog, i, transactionIndex, log, logsInTheBlock) 107 | }) 108 | transactionIndex += 1 109 | return decodedLogs 110 | }).filter(a => a.length > 0)) 111 | } 112 | 113 | // Flatten the logs array 114 | logs = logs.reduce((prev, curr) => { 115 | return prev.concat(curr) 116 | }, []).filter(a => a.length > 0) 117 | 118 | return logs 119 | } 120 | 121 | getLogsAsync (startBlock = this.eth.blockNumber, endBlock = this.eth.blockNumber) { 122 | return new Promise((resolve, reject) => { 123 | resolve(this.getLogs(startBlock, endBlock)) 124 | }) 125 | } 126 | 127 | /** 128 | ** Generator function which yields logs from the blockchain based on last 129 | ** read block and the latest block. 130 | **/ 131 | * getLogsGenerator () { 132 | let strictlyLess = true 133 | 134 | while (this.lastReadBlockNumber <= this.eth.blockNumber) { 135 | if (strictlyLess) { 136 | yield this.getLogs(this.lastReadBlockNumber, this.eth.blockNumber) 137 | } 138 | if (this.lastReadBlockNumber === this.eth.blockNumber) { 139 | strictlyLess = false 140 | // If there is no new block , yield empty list 141 | yield [] 142 | } else { 143 | strictlyLess = true 144 | this.lastReadBlockNumber = this.eth.blockNumber 145 | } 146 | } 147 | } 148 | 149 | /** 150 | ** Recursive polling function. (Recommended not to call this function 151 | ** directly.) Best usage pattern would be call turnOnPolling(handler). 152 | ** Termination condition for runPolling is when isPolling = false. 153 | **/ 154 | async runPolling (handler) { 155 | if (!this.isPolling) { 156 | // End polling if isPolling is set to false. 157 | return 158 | } 159 | 160 | if (!this.generator) { 161 | this.generator = this.getLogsGenerator() 162 | } 163 | // console.log('blocknumbers', this.lastReadBlockNumber, this.eth.blockNumber) 164 | const logsP = this.generator.next() 165 | if (logsP.value) { 166 | handler(null, logsP.value) 167 | } 168 | 169 | await wait(this.pollingInterval) 170 | this.runPolling(handler) 171 | } 172 | /** 173 | ** Turns polling on for reading logs. 174 | ** Params - 175 | ** handler: Expects an function handler to send the logs back. 176 | **/ 177 | turnOnPolling (handler, startBlock = this.eth.blockNumber) { 178 | this.isPolling = true 179 | this.lastReadBlockNumber = startBlock 180 | this.runPolling(handler) 181 | } 182 | 183 | /** 184 | ** Turns polling off.(Logs can still be manually read) 185 | **/ 186 | turnOffPolling () { 187 | this.isPolling = false 188 | } 189 | } 190 | 191 | module.exports = { 192 | Chainsaw 193 | } 194 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-chainsaw", 3 | "version": "1.1.4", 4 | "description": "\"Library for polling ethereum blockchain events and decoding the event logs.\" ", 5 | "main": "./dist/chainsaw.js", 6 | "scripts": { 7 | "test": "mocha ./tests/loader_test.js -R spec --timeout 200000", 8 | "build": "babel lib -d dist", 9 | "prepublish": "npm run build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/SpankChain/chainsaw.git" 14 | }, 15 | "author": "yogeshgo05", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/SpankChain/chainsaw/issues" 19 | }, 20 | "homepage": "https://github.com/SpankChain/chainsaw#readme", 21 | "devDependencies": { 22 | "babel-cli": "^6.26.0", 23 | "babel-eslint": "^6.0.2", 24 | "babel-preset-env": "^1.6.0", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-stage-2": "^6.5.0", 27 | "babel-register": "^6.18.0", 28 | "chai": "^3.5.0", 29 | "eslint": "^3.19.0", 30 | "eslint-config-standard": "^10.2.1", 31 | "eslint-plugin-import": "^2.2.0", 32 | "eslint-plugin-node": "^4.2.2", 33 | "eslint-plugin-promise": "^3.5.0", 34 | "eslint-plugin-standard": "^3.0.1", 35 | "ethereumjs-testrpc": "^3.9.2", 36 | "ethereumjs-tx": "^1.3.4" 37 | }, 38 | "dependencies": { 39 | "JSONStream": "^1.3.1", 40 | "abi-decoder": "^1.0.8", 41 | "bn": "^1.0.1", 42 | "body-parser": "^1.17.2", 43 | "chai": "^4.1.0", 44 | "es6-promisify": "^5.0.0", 45 | "es7": "^1.0.1", 46 | "ethereumjs-abi": "^0.6.4", 47 | "ethereumjs-util": "^5.1.2", 48 | "ethjs-contract": "^0.1.9", 49 | "ethjs-provider-http": "^0.1.6", 50 | "ethjs-query": "^0.2.9", 51 | "express": "^4.15.3", 52 | "mocha": "^3.4.2", 53 | "secp256k1": "^3.3.0", 54 | "simple-spinner": "0.0.5", 55 | "sinon": "^4.5.0", 56 | "solc": "^0.4.16", 57 | "util": "^0.10.3", 58 | "web3": "^0.19.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | mocha: true 4 | -------------------------------------------------------------------------------- /tests/chainsaw.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import p from 'es6-promisify' 3 | import Web3 from 'web3' 4 | import { sha3 } from 'ethereumjs-util' 5 | import { Chainsaw } from '../lib/chainsaw.js' 6 | import setup from './setup' 7 | import * as utils from './utils.js' 8 | import * as config from './config.json' 9 | 10 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) 11 | 12 | const web3 = new Web3(new Web3.providers.HttpProvider(config.WEB3_PROVIDER)) 13 | 14 | describe('chainsaw', () => { 15 | let chainsaw 16 | let contractInstance 17 | 18 | before(async () => { 19 | contractInstance = await setup({ 20 | testRPCProvider: config.WEB3_PROVIDER 21 | }) 22 | chainsaw = new Chainsaw(web3, [contractInstance.address]) 23 | chainsaw.addABI(contractInstance.abi) 24 | }) 25 | 26 | describe('[with test contract deployed ]', () => { 27 | 28 | it('[test contract deployment successful]', () => { 29 | assert.notEqual(contractInstance, undefined) 30 | assert.equal(contractInstance.address.length, 42) 31 | }) 32 | 33 | describe('[getLogsByBlockNumber: With single block range]', () => { 34 | it('[test with no (undecoded)logs/events]', async() => { 35 | await utils.mineBlocks(1) 36 | const logsInTheBlock = chainsaw.getLogsByBlockNumber(web3.eth.blockNumber) 37 | assert.equal(logsInTheBlock.length, 0, 'Since no contract method called ,hence no logs.') 38 | }) 39 | 40 | it('[Invalid block number]', () => { 41 | try { 42 | const logsInTheBlock = chainsaw.getLogsByBlockNumber(-1) 43 | } catch (error) { 44 | assert.equal(error.message, 'Invalid blockNumber : blockNumber should be greater than zero and less than latestBlock.') 45 | } 46 | }) 47 | 48 | it('[test with (undecoded)logs/events]', async () => { 49 | // Create an event in the block 50 | await contractInstance.createChannel(web3.eth.accounts[2], '0x222342') 51 | const logsInTheBlock = chainsaw.getLogsByBlockNumber(web3.eth.blockNumber)[0][0] 52 | assert.equal(logsInTheBlock.address.length, 42) 53 | assert.isArray(logsInTheBlock.topics, 'Topics should be an array') 54 | }) 55 | }) 56 | 57 | describe('[getLogs: no startBlock and endBlock (only default)]', () => { 58 | it('[getLogs: A block with no logs/events]', async () => { 59 | await utils.mineBlocks(1) 60 | assert.equal(chainsaw.getLogs().length, 0) 61 | }) 62 | 63 | it('[getLogs: A block with decoded logs/events]', async () => { 64 | // Create an event by calling method on the contract 65 | await contractInstance.createChannel(web3.eth.accounts[2], '0x222342') 66 | const eventLog = chainsaw.getLogs()[0][0] 67 | assert.equal(eventLog.name, 'DidCreateChannel') 68 | assert.equal(eventLog.address.length, 42) 69 | assert.isArray(eventLog.events) 70 | eventLog.events.forEach((elem) => { 71 | switch (elem.name) { 72 | case 'viewer': 73 | assert.equal(elem.value, web3.eth.accounts[0]) 74 | break 75 | case 'broadcaster': 76 | assert.equal(elem.value, web3.eth.accounts[2]) 77 | break 78 | case 'channelId': 79 | assert.equal(elem.value, '0x2223420000000000000000000000000000000000000000000000000000000000') 80 | break 81 | } 82 | }) 83 | }) 84 | }) 85 | describe('[getLogs: With startBlock and endBlock specified]', () => { 86 | it('[Get decoded logs/events for block range]', async() => { 87 | 88 | await utils.mineBlocks(1) 89 | const startBlock = web3.eth.blockNumber 90 | 91 | // Create a new event 92 | await contractInstance.createChannel(web3.eth.accounts[2], '0x222342') 93 | 94 | // Mine 3 no event Blocks 95 | await utils.mineBlocks(3) 96 | await contractInstance.deposit('0x222342', {value: 20}) 97 | 98 | const endBlock = web3.eth.blockNumber 99 | const logs = chainsaw.getLogs(startBlock, endBlock) 100 | 101 | logs.forEach((log) => { 102 | if (log[0].eventType === 'DidDeposit') { 103 | assert.equal(log[0].contractAddress, contractInstance.address) 104 | assert.equal(log[0].sender, web3.eth.accounts[0]) 105 | log[0].fields.forEach((elem) => { 106 | if (elem.name === 'channelId') { 107 | assert.equal(elem.value, '0x2223420000000000000000000000000000000000000000000000000000000000') 108 | } 109 | if (elem.name === 'amount') { 110 | assert.equal(elem.value, '20') 111 | } 112 | }) 113 | } 114 | 115 | if (log[0].eventType === 'DidCreateChannel') { 116 | assert.equal(log[0].contractAddress, contractInstance.address) 117 | assert.equal(log[0].sender, web3.eth.accounts[0]) 118 | 119 | log[0].fields.forEach((elem) => { 120 | switch (elem.name) { 121 | case 'viewer': 122 | assert.equal(elem.value, web3.eth.accounts[0]) 123 | break 124 | case 'broadcaster': 125 | assert.equal(elem.value, web3.eth.accounts[2]) 126 | break 127 | case 'channelId': 128 | assert.equal(elem.value, '0x2223420000000000000000000000000000000000000000000000000000000000') 129 | break 130 | } 131 | }) 132 | } 133 | }) 134 | }) 135 | 136 | it('[Test invalid block range]', () => { 137 | const startBlock = -1 138 | const endBlock = web3.eth.blockNumber + 100 139 | try { 140 | const logs = chainsaw.getLogs(startBlock, endBlock) 141 | } catch (error) { 142 | assert.equal(error.message, 'Invalid startBlock: Must be below web3.eth.blockNumber or startBlock cannot be below 0') 143 | } 144 | }) 145 | }) 146 | 147 | describe('[Test polling]', () => { 148 | const assertChainsawEvents = (_chainsaw) => { 149 | _chainsaw.turnOnPolling(function (error, response) { 150 | if (!error && response.length > 0) { 151 | response.forEach((log) => { 152 | if (log[0].eventType === 'DidDeposit') { 153 | assert.equal(log[0].contractAddress, contractInstance.address) 154 | assert.equal(log[0].sender, web3.eth.accounts[0]) 155 | log[0].fields.forEach((elem) => { 156 | if (elem.name === 'channelId') { 157 | assert.equal(elem.value, '0x2223420000000000000000000000000000000000000000000000000000000000') 158 | } 159 | if (elem.name === 'amount') { 160 | assert.equal(elem.value, '20') 161 | } 162 | }) 163 | } 164 | 165 | if (log[0].eventType === 'DidCreateChannel') { 166 | assert.equal(log[0].contractAddress, contractInstance.address) 167 | assert.equal(log[0].sender, web3.eth.accounts[0]) 168 | log[0].fields.forEach((elem) => { 169 | switch (elem.name) { 170 | case 'viewer': 171 | assert.equal(elem.value, web3.eth.accounts[0]) 172 | break 173 | case 'broadcaster': 174 | assert.equal(elem.value, web3.eth.accounts[2]) 175 | break 176 | case 'channelId': 177 | assert.equal(elem.value, '0x2223420000000000000000000000000000000000000000000000000000000000') 178 | break 179 | } 180 | }) 181 | } 182 | }) 183 | } 184 | }) 185 | _chainsaw.turnOffPolling() 186 | } 187 | 188 | it('[Basic Polling]', async () => { 189 | // Call an contract method to mine a new block 190 | await contractInstance.createChannel(web3.eth.accounts[2], '0x222342') 191 | await contractInstance.deposit('0x222342', {value: 20}) 192 | 193 | assertChainsawEvents(chainsaw) 194 | }) 195 | 196 | it('[Polling : PollingInterval specific testing]', async () => { 197 | // One second polling Interval 198 | const _localChainsaw = new Chainsaw(web3, [contractInstance.address], 1000) 199 | 200 | await contractInstance.createChannel(web3.eth.accounts[2], '0x222342') 201 | await contractInstance.deposit('0x222342', {value: 20}) 202 | 203 | // Wait for 1 second of polling interval 204 | await wait(1000) 205 | assertChainsawEvents(_localChainsaw) 206 | }) 207 | 208 | it('[Polling : Empty polling (no new block created)]', async () => { 209 | await utils.mineBlocks(1) 210 | const _localChainsaw = new Chainsaw(web3, [contractInstance.address], 1000) 211 | 212 | // Turn on polling 213 | _localChainsaw.turnOnPolling((error, response) => { 214 | if (!error) { 215 | assert.equal(response.length, 0, 'New block is empty with no logs') 216 | } 217 | }) 218 | 219 | // Turn off polling. 220 | _localChainsaw.turnOffPolling() 221 | // Turn on polling. 222 | _localChainsaw.turnOnPolling((error, response) => { 223 | if (!error) { 224 | assert.equal(response.length, 0, 'Reads just the last block again') 225 | } 226 | }) 227 | }) 228 | }) 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /tests/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "WEB3_PROVIDER": "http://localhost:8545" 3 | } 4 | -------------------------------------------------------------------------------- /tests/contracts/StubPaymentChannel.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.2; 2 | 3 | contract StubPaymentChannel{ 4 | enum ChannelState { Open, Settling, Settled } 5 | 6 | struct PaymentChannel 7 | { 8 | address sender ; 9 | address broadcaster ; 10 | 11 | } 12 | 13 | event DidCreateChannel(address indexed viewer, 14 | address indexed broadcaster, 15 | bytes32 channelId); 16 | 17 | event DidDeposit(bytes32 indexed channelId, 18 | uint256 amount); 19 | 20 | event DidStartSettle(bytes32 indexed channelId, 21 | uint256 payment); 22 | 23 | event DidSettle(bytes32 channelId, 24 | uint256 payment, 25 | uint256 challengedPayment); 26 | 27 | event DidChannelClose(bytes32 channelId); 28 | 29 | 30 | /*function StubPaymentChannel() { 31 | 32 | }*/ 33 | 34 | function createChannel(address broadcaster, bytes32 channelId) 35 | public payable{ 36 | 37 | // Logic of channel Id saving . 38 | 39 | // Trigger create channel event . 40 | DidCreateChannel(msg.sender, broadcaster, channelId); 41 | } 42 | 43 | function deposit(bytes32 channelId) public payable{ 44 | // Logic of Deposit here 45 | 46 | // Trigger the event DidDeposit. 47 | DidDeposit(channelId, msg.value) ; 48 | } 49 | 50 | function startSettle(bytes32 channelId, uint256 payment) public payable{ 51 | 52 | // Logic for startSettle goes here . 53 | 54 | // Trigger the event DidSettle. 55 | DidStartSettle(channelId, payment); 56 | } 57 | 58 | function settle(bytes32 channelId , uint256 payment, uint256 challengedPayment) public payable{ 59 | 60 | // Logic for settle . 61 | 62 | // Trigger the event DidSettle 63 | DidSettle(channelId, payment, challengedPayment); 64 | } 65 | 66 | function close(bytes32 channelId) 67 | { 68 | // Logic closing . 69 | 70 | DidChannelClose(channelId); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/loader_test.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | require('./chainsaw.js') 3 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import p from 'es6-promisify' 3 | import TestRPC from 'ethereumjs-testrpc' 4 | import solc from 'solc' 5 | import Eth from 'ethjs-query' 6 | import EthContract from 'ethjs-contract' 7 | import Web3 from 'web3' 8 | import HttpProvider from 'ethjs-provider-http' 9 | 10 | const Tx = require('ethereumjs-tx') 11 | 12 | const SOL_PATH = __dirname + '/contracts/' 13 | const TESTRPC_PORT = 8545 14 | const MNEMONIC = 'elegant ability lawn fiscal fossil general swarm trap bind require exchange ostrich' 15 | const DEFAULT_CONTRACT = 'StubPaymentChannel.sol' 16 | 17 | // opts 18 | // testRPCServer - if true, starts a testRPC server 19 | // mnemonic - seed for accounts 20 | // port - testrpc port 21 | // noDeploy - if true, skip auction contract deployment 22 | // testRPCProvider - http connection string for console testprc instance 23 | // defaultContract - default contract to deploy . 24 | export default async function (opts) { 25 | opts = opts || {} 26 | const mnemonic = opts.mnemonic || MNEMONIC 27 | const testRPCServer = opts.testRPCServer 28 | const port = opts.port || TESTRPC_PORT 29 | const noDeploy = opts.noDeploy 30 | const defaultAcct = opts.defaultAcct ? opts.defaultAcct : 0 31 | const defaultContract = opts.defaultContract || DEFAULT_CONTRACT 32 | // const input = opts.input || {defaultContract: fs.readFileSync(SOL_PATH + defaultContract).toString()} 33 | const defaultContractFormat = `${defaultContract}:` + defaultContract.slice(0, defaultContract.indexOf('.')) 34 | 35 | // START TESTRPC PROVIDER 36 | let provider 37 | if (opts.testRPCProvider) { 38 | provider = new HttpProvider(opts.testRPCProvider) 39 | } else { 40 | provider = TestRPC.provider({ 41 | mnemonic: mnemonic 42 | }) 43 | } 44 | // START TESTRPC SERVER 45 | if (opts.testRPCServer) { 46 | await p(TestRPC.server({ 47 | mnemonic: mnemonic 48 | }).listen)(port) 49 | } 50 | 51 | // BUILD ETHJS ABSTRACTIONS 52 | const eth = new Eth(provider) 53 | const contract = new EthContract(eth) 54 | const accounts = await eth.accounts() 55 | 56 | // COMPILE THE CONTRACT 57 | const input = {} 58 | input[defaultContract] = fs.readFileSync(SOL_PATH + defaultContract).toString() 59 | const output = solc.compile({ sources: input }, 1) 60 | if (output.errors) { console.log(Error(output.errors)) } 61 | 62 | const abi = JSON.parse(output.contracts[defaultContractFormat].interface) 63 | const bytecode = output.contracts[defaultContractFormat].bytecode 64 | 65 | // PREPARE THE CONTRACT ABSTRACTION OBJECT 66 | const contractInstance = contract(abi, bytecode, { 67 | from: accounts[defaultAcct], 68 | gas: 3000000 69 | }) 70 | 71 | let contractTxHash, contractReceipt, contractObject 72 | if (!noDeploy) { 73 | // DEPLOY THE CONTRACT 74 | contractTxHash = await contractInstance.new() 75 | await wait(1500) 76 | // USE THE ADDRESS FROM THE TX RECEIPT TO BUILD THE CONTRACT OBJECT 77 | contractReceipt = await eth.getTransactionReceipt(contractTxHash) 78 | contractObject = contractInstance.at(contractReceipt.contractAddress) 79 | } 80 | 81 | // MAKE WEB3 82 | const web3 = new Web3() 83 | web3.setProvider(provider) 84 | web3.eth.defaultAccount = accounts[0] 85 | 86 | return contractObject 87 | } 88 | 89 | export async function setupAuction (opts) { 90 | opts = opts || {} 91 | const mnemonic = opts.mnemonic || MNEMONIC 92 | const testRPCServer = opts.testRPCServer 93 | const port = opts.port || TESTRPC_PORT 94 | const noDeploy = opts.noDeploy 95 | const defaultAcct = opts.defaultAcct ? opts.defaultAcct : 0 96 | const defaultContract = opts.defaultContract || DEFAULT_CONTRACT 97 | const input = opts.input || {defaultContract: fs.readFileSync(SOL_PATH + defaultContract).toString()} 98 | const constructParams = opts.constructParams || {} 99 | const defaultContractFormat = `${defaultContract}:` + defaultContract.slice(0, defaultContract.indexOf('.')) 100 | 101 | // START TESTRPC PROVIDER 102 | let provider 103 | if (opts.testRPCProvider) { 104 | provider = new HttpProvider(opts.testRPCProvider) 105 | } else { 106 | provider = TestRPC.provider({ 107 | mnemonic: mnemonic 108 | }) 109 | } 110 | // START TESTRPC SERVER 111 | if (opts.testRPCServer) { 112 | await p(TestRPC.server({ 113 | mnemonic: mnemonic 114 | }).listen)(port) 115 | } 116 | 117 | // BUILD ETHJS ABSTRACTIONS 118 | const eth = new Eth(provider) 119 | const contract = new EthContract(eth) 120 | const accounts = await eth.accounts() 121 | 122 | // COMPILE THE CONTRACT 123 | // const input = {} 124 | // input[defaultContract] = fs.readFileSync(SOL_PATH + defaultContract).toString() 125 | const output = solc.compile({ sources: input }, 1) 126 | if (output.errors) { console.log(Error(output.errors)) } 127 | 128 | const abi = JSON.parse(output.contracts[defaultContractFormat].interface) 129 | const bytecode = output.contracts[defaultContractFormat].bytecode 130 | 131 | // PREPARE THE CONTRACT ABSTRACTION OBJECT 132 | const contractInstance = contract(abi, bytecode, { 133 | from: accounts[defaultAcct], 134 | gas: 3000000 135 | }) 136 | let contractTxHash, contractReceipt, contractObject 137 | if (!noDeploy) { 138 | // DEPLOY THE AUCTION CONTRACT 139 | contractTxHash = await contractInstance.new( 140 | constructParams['tokenSupply'], 141 | constructParams['tokenName'], 142 | constructParams['tokenDecimals'], 143 | constructParams['tokenSymbol'], 144 | constructParams['weiWallet'], 145 | constructParams['tokenWallet'], 146 | constructParams['minDepositInWei'], 147 | constructParams['minWeiToRaise'], 148 | constructParams['maxWeiToRaise'], 149 | constructParams['minTokensForSale'], 150 | constructParams['maxTokensForSale'], 151 | constructParams['maxTokenBonusPercentage'], 152 | constructParams['depositWindowInBlocks'], 153 | constructParams['processingWindowInBlocks'] 154 | ) 155 | await wait(1500) 156 | // USE THE ADDRESS FROM THE TX RECEIPT TO BUILD THE CONTRACT OBJECT 157 | contractReceipt = await eth.getTransactionReceipt(contractTxHash) 158 | contractObject = contractInstance.at(contractReceipt.contractAddress) 159 | } 160 | 161 | // MAKE WEB3 162 | const web3 = new Web3() 163 | web3.setProvider(provider) 164 | web3.eth.defaultAccount = accounts[0] 165 | 166 | return contractObject 167 | } 168 | 169 | // async/await compatible setTimeout 170 | // http://stackoverflow.com/questions/38975138/is-using-async-in-settimeout-valid 171 | // await wait(2000) 172 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) 173 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import p from 'es6-promisify' 3 | const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) 4 | 5 | export const takeSnapshot = () => { 6 | return new Promise(async (accept) => { 7 | let res = await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 8 | jsonrpc: '2.0', 9 | method: 'evm_snapshot', 10 | id: new Date().getTime() 11 | }) 12 | accept(res.result) 13 | }) 14 | } 15 | 16 | export const revertSnapshot = (snapshotId) => { 17 | return new Promise(async (accept) => { 18 | await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 19 | jsonrpc: '2.0', 20 | method: 'evm_revert', 21 | params: [snapshotId], 22 | id: new Date().getTime() 23 | }) 24 | accept() 25 | }) 26 | } 27 | 28 | export const mineBlock = () => { 29 | return new Promise(async (accept) => { 30 | await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 31 | jsonrpc: "2.0", 32 | method: "evm_mine", 33 | id: new Date().getTime() 34 | }) 35 | accept() 36 | }) 37 | } 38 | 39 | export const mineBlocks = (count) => { 40 | return new Promise(async (accept) => { 41 | let i = 0 42 | while (i < count) { 43 | await mineBlock() 44 | i++ 45 | } 46 | accept() 47 | }) 48 | } 49 | 50 | export const randomIntFromInterval = (min, max) => { 51 | return Math.floor(Math.random() * (max - min + 1) + min) 52 | } 53 | --------------------------------------------------------------------------------