├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── Migrations.sol └── TokenBatchTransfer.sol ├── migrations ├── 1_initial_migration.js └── 3_TokenBatchTransfer.js ├── package-lock.json ├── package.json ├── resources └── npm-README.md ├── scripts ├── package-npm.js └── truffle-migrate ├── test ├── TokenBatchTransferTests.js ├── sign_funcs.js └── util │ └── Util.js ├── truffle-config.js └── truffle.js /.gitignore: -------------------------------------------------------------------------------- 1 | build/npm-module 2 | build 3 | node_modules 4 | yarn-error.log 5 | npm-debug.log 6 | addresses.json 7 | /.vscode 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SingularityNET 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TokenBatchTransfer-Contract 2 | Token Batch Transfer Smart Contract 3 | 4 | # High Level Functional Requirements 5 | 6 | Placeholder to place the high level requirements 7 | 8 | ## Contracts 9 | 10 | ### TokenBatchTransfer 11 | * TokenBatchTransfer contract is to enable token owner to perform a batch transfer of the ERC20 tokens. 12 | 13 | ## Software Requirements 14 | * [Node.js](https://github.com/nodejs/node) (8+) 15 | * [Npm](https://www.npmjs.com/package/npm) 16 | 17 | ## Deployed Contracts (npm version 0.1.0) 18 | 19 | * TokenBatchTransfer (Mainnet): [Address Placeholder] (etherscan URL PLaceholder) 20 | * TokenBatchTransfer (Ropsten): [Address Placeholder] (etherscan URL PLaceholder) 21 | * TokenBatchTransfer (Kovan): [Address Placeholder] (etherscan URL PLaceholder) 22 | 23 | ## Install 24 | 25 | ### Dependencies 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | ### Compile 31 | ```bash 32 | truffle compile 33 | ``` 34 | 35 | ### Test 36 | ```bash 37 | truffle test 38 | ``` 39 | 40 | ## Package 41 | ```bash 42 | npm run package-npm 43 | ``` 44 | 45 | ## Release 46 | Contract build artifacts are published to NPM: https://www.npmjs.com/package/batch-token-transfer 47 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.8.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/TokenBatchTransfer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | import "@openzeppelin/contracts/math/SafeMath.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract TokenBatchTransfer is Ownable { 8 | 9 | using SafeMath for uint256; 10 | 11 | ERC20 public token; // Address of token contract 12 | address public transferOperator; // Address to manage the Transfers 13 | 14 | // Modifiers 15 | modifier onlyOperator() { 16 | require( 17 | msg.sender == transferOperator, 18 | "Only operator can call this function." 19 | ); 20 | _; 21 | } 22 | 23 | constructor(address _token) 24 | public 25 | { 26 | token = ERC20(_token); 27 | transferOperator = msg.sender; 28 | } 29 | 30 | 31 | // Events 32 | event NewOperator(address transferOperator); 33 | event WithdrawToken(address indexed owner, uint256 stakeAmount); 34 | 35 | function updateOperator(address newOperator) public onlyOwner { 36 | 37 | require(newOperator != address(0), "Invalid operator address"); 38 | 39 | transferOperator = newOperator; 40 | 41 | emit NewOperator(newOperator); 42 | } 43 | 44 | // To withdraw tokens from contract, to deposit directly transfer to the contract 45 | function withdrawToken(uint256 value) public onlyOperator 46 | { 47 | 48 | // Check if contract is having required balance 49 | require(token.balanceOf(address(this)) >= value, "Not enough balance in the contract"); 50 | require(token.transfer(msg.sender, value), "Unable to transfer token to the owner account"); 51 | 52 | emit WithdrawToken(msg.sender, value); 53 | 54 | } 55 | 56 | // To transfer tokens from Contract to the provided list of token holders with respective amount 57 | function batchTransfer(address[] calldata tokenHolders, uint256[] calldata amounts) 58 | external 59 | onlyOperator 60 | { 61 | require(tokenHolders.length == amounts.length, "Invalid input parameters"); 62 | 63 | for(uint256 indx = 0; indx < tokenHolders.length; indx++) { 64 | require(token.transfer(tokenHolders[indx], amounts[indx]), "Unable to transfer token to the account"); 65 | } 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | let Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/3_TokenBatchTransfer.js: -------------------------------------------------------------------------------- 1 | let TokenBatchTransfer = artifacts.require("./TokenBatchTransfer.sol"); 2 | let Contract = require("@truffle/contract"); 3 | let TokenAbi = require("singularitynet-token-contracts/abi/SingularityNetToken.json"); 4 | let TokenNetworks = require("singularitynet-token-contracts/networks/SingularityNetToken.json"); 5 | let TokenBytecode = require("singularitynet-token-contracts/bytecode/SingularityNetToken.json"); 6 | let Token = Contract({contractName: "SingularityNetToken", abi: TokenAbi, networks: TokenNetworks, bytecode: TokenBytecode}); 7 | 8 | // Token Contract Constants 9 | const name = "SingularityNet Token" 10 | const symbol = "AGIX" 11 | 12 | module.exports = function(deployer, network, accounts) { 13 | Token.setProvider(web3.currentProvider) 14 | Token.defaults({from: accounts[0], gas: 4000000}); 15 | 16 | // AGI-I Contract deployment -- Will be deleted once AGI-2 is deployed - Kept it for compatibility only 17 | //deployer.deploy(Token, {overwrite: false, gas: 4000000}).then((TokenInstance) => deployer.deploy(TokenBatchTransfer, TokenInstance.address)); 18 | 19 | // AGI-II Contract deployment 20 | deployer.deploy(Token, name, symbol, {overwrite: false, gas: 4000000}).then((TokenInstance) => deployer.deploy(TokenBatchTransfer, TokenInstance.address)); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "singularitynet-batch-transfer", 3 | "version": "0.1.1", 4 | "private": true, 5 | "description": "Includes token batch transfer contracts, migrations, tests", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "package-npm": "node scripts/package-npm.js", 11 | "deploy": "scripts/truffle-migrate", 12 | "compile": "node_modules/.bin/truffle compile", 13 | "test": "rm -rf build && node_modules/.bin/truffle test", 14 | "test-develop": "rm -rf build && node_modules/.bin/truffle test --network develop" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/singnet/batch-token-transfer.git" 19 | }, 20 | "author": "SingularityNET", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/singnet/batch-token-transfer/issues" 24 | }, 25 | "homepage": "https://github.com/singnet/batch-token-transfer.git#readme", 26 | "dependencies": { 27 | "@openzeppelin/contracts": "^3.2.0", 28 | "@truffle/contract": "^4.2.23", 29 | "@truffle/hdwallet-provider": "^1.1.0", 30 | "singularitynet-token-contracts": "3.0.0", 31 | "truffle": "5.1.46" 32 | }, 33 | "devDependencies": { 34 | "chai": "^4.2.0", 35 | "chai-as-promised": "^7.1.1", 36 | "chai-bignumber": "^2.0.2", 37 | "eth-gas-reporter": "^0.1.12", 38 | "ethereumjs-abi": "^0.6.5", 39 | "ethereumjs-util": "^5.2.0", 40 | "fs-extra": "^5.0.0", 41 | "moment": "^2.22.2", 42 | "webpack-cli": "^3.1.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/npm-README.md: -------------------------------------------------------------------------------- 1 | # staking-contracts 2 | STAKING Contracts 3 | -------------------------------------------------------------------------------- /scripts/package-npm.js: -------------------------------------------------------------------------------- 1 | "use-strict"; 2 | 3 | const fse = require("fs-extra"); 4 | const path = require("path"); 5 | const npmModulePath = "build/npm-module"; 6 | const packageJson = "package.json"; 7 | 8 | // source dir relative to repo root => dest dir relative to module root 9 | const mapDirs = { 10 | }; 11 | 12 | // source file relative to repo root => dest file relative to module root 13 | // or => key to extract from source file => dest file relative to module root 14 | const mapFiles = { 15 | "contracts/TokenBatchTransfer.sol": "sol/TokenBatchTransfer.sol", 16 | "build/contracts/TokenBatchTransfer.json": { 17 | "abi": "abi/TokenBatchTransfer.json", 18 | "networks": "networks/TokenBatchTransfer.json", 19 | "bytecode": "bytecode/TokenBatchTransfer.json" 20 | }, 21 | "resources/npm-README.md": "README.md", 22 | "LICENSE": "LICENSE" 23 | }; 24 | 25 | let transformPackageJson = (x) => { 26 | return { 27 | name: x.name, 28 | version: x.version, 29 | description: x.description, 30 | repository: x.repository, 31 | author: x.author, 32 | license: x.license, 33 | bugs: x.bugs, 34 | homepage: x.homepage, 35 | dependencies: { 36 | "singularitynet-token-contracts": x.dependencies["singularitynet-token-contracts"], 37 | "openzeppelin-solidity": x.dependencies["openzeppelin-solidity"] 38 | } 39 | }; 40 | }; 41 | 42 | fse.removeSync(npmModulePath); 43 | fse.mkdirsSync(npmModulePath); 44 | 45 | for (let sourceDir in mapDirs) { 46 | let destDir = path.join(npmModulePath, mapDirs[sourceDir]); 47 | let destParent = path.resolve(destDir, "../"); 48 | fse.mkdirsSync(destParent); 49 | fse.copySync(sourceDir, destDir); 50 | } 51 | 52 | for (let sourceFile in mapFiles) { 53 | if (mapFiles[sourceFile] !== null && typeof mapFiles[sourceFile] === "object") { 54 | for (key in mapFiles[sourceFile]) { 55 | let destFile = path.join(npmModulePath, mapFiles[sourceFile][key]); 56 | let destParent = path.resolve(destFile, "../"); 57 | fse.mkdirsSync(destParent); 58 | fse.writeJsonSync(destFile, fse.readJsonSync(sourceFile)[key]); 59 | } 60 | } else { 61 | let destFile = path.join(npmModulePath, mapFiles[sourceFile]); 62 | let destParent = path.resolve(destFile, "../"); 63 | fse.mkdirsSync(destParent); 64 | fse.copySync(sourceFile, destFile); 65 | } 66 | } 67 | 68 | let packageJsonIn = fse.readJsonSync(packageJson); 69 | fse.writeJsonSync(path.join(npmModulePath, packageJson), transformPackageJson(packageJsonIn), {spaces: 4}); 70 | -------------------------------------------------------------------------------- /scripts/truffle-migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Enter BIP-39 mnemonic: " 4 | read HDWALLET_MNEMONIC 5 | echo "Enter target network name (as defined in truffle.js): " 6 | read NETWORK 7 | HDWALLET_MNEMONIC=$HDWALLET_MNEMONIC node_modules/.bin/truffle migrate --network $NETWORK 8 | -------------------------------------------------------------------------------- /test/TokenBatchTransferTests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var TokenBatchTransfer = artifacts.require("./TokenBatchTransfer.sol"); 3 | 4 | let Contract = require("@truffle/contract"); 5 | let TokenAbi = require("singularitynet-token-contracts/abi/SingularityNetToken.json"); 6 | let TokenNetworks = require("singularitynet-token-contracts/networks/SingularityNetToken.json"); 7 | let TokenBytecode = require("singularitynet-token-contracts/bytecode/SingularityNetToken.json"); 8 | let Token = Contract({contractName: "SingularityNetToken", abi: TokenAbi, networks: TokenNetworks, bytecode: TokenBytecode}); 9 | Token.setProvider(web3.currentProvider); 10 | 11 | var ethereumjsabi = require('ethereumjs-abi'); 12 | var ethereumjsutil = require('ethereumjs-util'); 13 | let signFuns = require('./sign_funcs'); 14 | 15 | const { assert } = require("chai"); 16 | 17 | async function testErrorRevert(prom) 18 | { 19 | let rezE = -1 20 | try { await prom } 21 | catch(e) { 22 | rezE = e.message.indexOf('revert'); 23 | //console.log("Catch Block: " + e.message); 24 | } 25 | assert(rezE >= 0, "Must generate error and error message must contain revert"); 26 | } 27 | 28 | contract('TokenBatchTransfer', function(accounts) { 29 | 30 | console.log("Number of Accounts - ", accounts.length) 31 | 32 | var tokenBatchTransfer; 33 | var tokenAddress; 34 | var token; 35 | 36 | let GAmt = 10000 * 100000000; 37 | 38 | 39 | before(async () => 40 | { 41 | tokenBatchTransfer = await TokenBatchTransfer.deployed(); 42 | tokenAddress = await tokenBatchTransfer.token.call(); 43 | token = await Token.at(tokenAddress); 44 | }); 45 | 46 | 47 | 48 | const approveTokensToContract = async(_startAccountIndex, _endAccountIndex, _depositAmt) => { 49 | // Transfer & Approve amount for respective accounts to Contract Address 50 | for(var i=_startAccountIndex;i<=_endAccountIndex;i++) { 51 | await token.transfer(accounts[i], _depositAmt, {from:accounts[0]}); 52 | await token.approve(tokenBatchTransfer.address,_depositAmt, {from:accounts[i]}); 53 | } 54 | 55 | }; 56 | 57 | const updateTransferOperatorAndVeryfy = async(_transferOperator, _account) => { 58 | 59 | await tokenBatchTransfer.updateOperator(_transferOperator, {from:_account}); 60 | 61 | // Get the Updated Transfer Operator 62 | const transferOperator = await tokenBatchTransfer.transferOperator.call(); 63 | assert.equal(transferOperator, _transferOperator); 64 | 65 | } 66 | 67 | const updateOwnerAndVerify = async(_newOwner, _account) => { 68 | 69 | let newOwner = "0x0" 70 | 71 | const owner_b = await tokenBatchTransfer.owner.call(); 72 | await tokenBatchTransfer.transferOwnership(_newOwner, {from:_account}); 73 | 74 | // Following lines of code if for Claimable Contract - which extends ownable functionality 75 | /* 76 | // Owner should not be updated until new Owner Accept the Ownership 77 | newOwner = await tokenBatchTransfer.owner.call(); 78 | assert.equal(newOwner, owner_b); 79 | 80 | // Call the function to accept the ownership 81 | await tokenBatchTransfer.claimOwnership({from:_newOwner}); 82 | */ 83 | newOwner = await tokenBatchTransfer.owner.call(); 84 | 85 | assert.equal(newOwner, _newOwner); 86 | 87 | } 88 | 89 | const withdrawTokenAndVerify = async(_amount, _account) => { 90 | 91 | // Token Balance 92 | const wallet_bal_b = (await token.balanceOf(_account)).toNumber(); 93 | const contract_bal_b = (await token.balanceOf(tokenBatchTransfer.address)).toNumber(); 94 | 95 | // Call Withdraw Stake 96 | await tokenBatchTransfer.withdrawToken(_amount, {from:_account}); 97 | 98 | // Token Balance 99 | const wallet_bal_a = (await token.balanceOf(_account)).toNumber(); 100 | const contract_bal_a = (await token.balanceOf(tokenBatchTransfer.address)).toNumber(); 101 | 102 | // Wallet Balance Should Increase 103 | assert.equal(wallet_bal_b, wallet_bal_a - _amount); 104 | 105 | // Contract Balance Should Reduce 106 | assert.equal(contract_bal_b, contract_bal_a + _amount); 107 | 108 | } 109 | 110 | 111 | const getRandomNumber = (max) => { 112 | const min = 10; // To avoid zero rand number 113 | return Math.floor(Math.random() * (max - min) + min); 114 | } 115 | 116 | const sleep = async (sec) => { 117 | console.log("Waiting for cycle to complete...Secs - " + sec); 118 | return new Promise((resolve) => { 119 | setTimeout(resolve, sec * 1000); 120 | }); 121 | } 122 | 123 | 124 | // ************************ Test Scenarios Starts From Here ******************************************** 125 | 126 | 127 | 128 | 129 | 130 | it("0. Initial Account Setup - Transfer & Approve Tokens", async function() 131 | { 132 | // accounts[0] -> Contract Owner 133 | 134 | // An explicit call is required to mint the tokens for AGI-II 135 | await token.mint(accounts[0], 2*GAmt, {from:accounts[0]}); 136 | 137 | //await approveTokensToContract(1, 9, GAmt); 138 | 139 | // Deposit tokens to the contract for the batch transfer 140 | await token.transfer(tokenBatchTransfer.address, GAmt, {from:accounts[0]}); 141 | 142 | }); 143 | 144 | it("1. Administrative Operations - Update Owner", async function() 145 | { 146 | 147 | // Change the Owner to Accounts[1] 148 | await updateOwnerAndVerify(accounts[1], accounts[0]); 149 | 150 | // Revert to back the ownership to accounts[0] 151 | await updateOwnerAndVerify(accounts[0], accounts[1]); 152 | 153 | // Owner Cannot be updated by any other user 154 | await testErrorRevert(tokenBatchTransfer.transferOwnership(accounts[1], {from:accounts[2]})); 155 | 156 | }); 157 | 158 | it("2. Administrative Operations - Update Transfer Operator", async function() 159 | { 160 | 161 | // Update the Transfer Operator to accounts[9] 162 | await updateTransferOperatorAndVeryfy(accounts[9], accounts[0]); 163 | 164 | // Transfer Operator should be uodated only by Owner 165 | await testErrorRevert(tokenBatchTransfer.updateOperator(accounts[8], {from:accounts[1]})); 166 | 167 | // Even the Oprator cannot update to another operator 168 | await testErrorRevert(tokenBatchTransfer.updateOperator(accounts[8], {from:accounts[9]})); 169 | 170 | }); 171 | 172 | it("3. Batch Transfers - 1", async function() 173 | { 174 | 175 | let tokenHolders = []; 176 | let amounts = []; 177 | let max = 100; 178 | 179 | for(var i=1; i setTimeout(resolve, ms)); 8 | } 9 | 10 | 11 | function signMessage(authorizerAccount, message, callback) 12 | { 13 | web3.eth.sign("0x" + message.toString("hex"),authorizerAccount,callback); 14 | } 15 | 16 | 17 | function signMessageFromAuthorizer(authorizerAccount, amount, tokenHolder, blockNumber, contractAddress, sourceAddress, callback) 18 | { 19 | var sigPrefix = "__conversion"; 20 | var message = ethereumjsabi.soliditySHA3( 21 | ["string", "uint256", "address", "uint256", "address", "bytes"], 22 | [sigPrefix, amount, tokenHolder, blockNumber, contractAddress, sourceAddress]); 23 | 24 | signMessage(authorizerAccount, message, callback); 25 | 26 | } 27 | 28 | 29 | // this mimics the prefixing behavior of the ethSign JSON-RPC method. 30 | function prefixed(hash) { 31 | return ethereumjsabi.soliditySHA3( 32 | ["string", "bytes32"], 33 | ["\x19Ethereum Signed Message:\n32", hash] 34 | ); 35 | } 36 | 37 | function recoverSigner(message, signature) { 38 | var split = ethereumjsutil.fromRpcSig(signature); 39 | var publicKey = ethereumjsutil.ecrecover(message, split.v, split.r, split.s); 40 | 41 | var signer = ethereumjsutil.pubToAddress(publicKey).toString("hex"); 42 | return signer; 43 | } 44 | 45 | function isValidSignatureClaim(contractAddress, channelId, nonce, amount, signature, expectedSigner) { 46 | var message = prefixed(composeClaimMessage(contractAddress, channelId, nonce, amount)); 47 | var signer = recoverSigner(message, signature); 48 | return signer.toLowerCase() == 49 | ethereumjsutil.stripHexPrefix(expectedSigner).toLowerCase(); 50 | } 51 | 52 | function getVRSFromSignature(signature) { 53 | signature = signature.substr(2); //remove 0x 54 | const r = '0x' + signature.slice(0, 64); 55 | const s = '0x' + signature.slice(64, 128); 56 | const v = '0x' + signature.slice(128, 130); // Should be either 27 or 28 57 | const v_decimal = web3.utils.toDecimal(v); 58 | const v_compute = (web3.utils.toDecimal(v) < 27 ) ? v_decimal + 27 : v_decimal ; 59 | 60 | return { 61 | v: v_compute, 62 | r: r, 63 | s: s 64 | }; 65 | 66 | } 67 | 68 | async function waitSignedMessage(authorizerAccount, amount, tokenHolder, blockNumber, contractAddress, sourceAddress) 69 | { 70 | let detWait = true; 71 | let rezSign; 72 | signMessageFromAuthorizer(authorizerAccount, amount, tokenHolder, blockNumber, contractAddress, sourceAddress, function(err,sgn) 73 | { 74 | detWait = false; 75 | rezSign = sgn 76 | }); 77 | while(detWait) 78 | { 79 | await sleep(1) 80 | } 81 | return rezSign; 82 | } 83 | 84 | module.exports.waitSignedMessage = waitSignedMessage; 85 | module.exports.isValidSignatureClaim = isValidSignatureClaim; 86 | module.exports.getVRSFromSignature = getVRSFromSignature; 87 | -------------------------------------------------------------------------------- /test/util/Util.js: -------------------------------------------------------------------------------- 1 | const STRINGS = Object.freeze({ 2 | "NULL_ADDRESS" : "0x0000000000000000000000000000000000000000", 3 | "REGISTRY_ERC165_ID" : "0x256b3545" 4 | }); 5 | 6 | /** 7 | * Enums are not supported by the ABI, they are just supported by Solidity. 8 | * You have to do the mapping yourself for now, we might provide some help later. 9 | * 10 | * https://solidity.readthedocs.io/en/latest/frequently-asked-questions.html 11 | */ 12 | const AGENT_STATE = Object.freeze({ 13 | "ENABLED" : 0, 14 | "DISABLED" : 1 15 | }); 16 | 17 | const JOB_STATE = Object.freeze({ 18 | "PENDING" : 0, 19 | "FUNDED" : 1, 20 | "COMPLETED" : 2 21 | }); 22 | 23 | const TX_STATUS = Object.freeze({ 24 | "FAILURE" : 0, 25 | "SUCCESS" : 1 26 | }); 27 | 28 | const HELPERS = Object.freeze({ 29 | signAddress : (address, account) => { 30 | let sig = web3.eth.sign(account, web3.fromUtf8(address)).slice(2); 31 | let r = `0x${sig.slice(0, 64)}`; 32 | let s = `0x${sig.slice(64, 128)}`; 33 | let v = web3.toDecimal(sig.slice(128, 130)) + 27; 34 | 35 | return [v, r, s]; 36 | }, 37 | /** 38 | * Trims character from the beginning and end of string. Useful for trimming \u0000 from smart contract fields. 39 | */ 40 | trimByChar : (string, character) => { 41 | const first = [...string].findIndex(char => char !== character); 42 | const last = [...string].reverse().findIndex(char => char !== character); 43 | return string.substring(first, string.length - last); 44 | }, 45 | /** 46 | * Convert bytes retrieved from the EVM to a usable/printable javascript string. 47 | */ 48 | bytesToString : (string) => HELPERS.trimByChar(web3.toAscii(string), '\0'), 49 | /** 50 | * Convert an address retrieved from the EVM to a usable/printable javascript string. 51 | */ 52 | addressToString : (address) => HELPERS.trimByChar(address, '\0'), 53 | /** 54 | * Returns an array like [start, start + 1, ..., end] 55 | */ 56 | intRange : (start, end) => { 57 | return [...Array(end-start).keys()] // generates array [0, 1, ..., (end-start) 58 | .map(e => start + e); // converts array to [start, start + 1, ..., end] 59 | }, 60 | /** 61 | * Compares that the arrays given are 'equal' in length and elements according to the assertEqualFn function. 62 | */ 63 | assertArraysEqual : (assertEqualFn, arrayExpected, arrayActual, message) => { 64 | assertEqualFn(arrayExpected.length, arrayActual.length, message + ": Array length mismatch."); 65 | 66 | const arrayExpectedSorted = arrayExpected.sort(); 67 | const arrayActualSorted = arrayActual.sort(); 68 | 69 | // console.log("Expected ", arrayExpectedSorted); 70 | // console.log("Actual ", arrayActualSorted); 71 | 72 | for (let i = 0; i < arrayExpectedSorted.length; i++) { 73 | assertEqualFn(arrayExpectedSorted[i], arrayActualSorted[i], message + `: sorted arrays differ at element ${i}`); 74 | } 75 | }, 76 | /** 77 | * Takes a truffle contract object and returns an array of all function selectors 78 | * 79 | * e.g. 80 | * 81 | * [ 82 | * 'createOrganization(bytes32,address[])', 83 | * 'changeOrganizationOwner(bytes32,address)', 84 | * ... 85 | * ] 86 | */ 87 | generateSelectorArray : (contract) => 88 | contract.abi 89 | .filter(x => x.type === 'function') // filter only functions 90 | .map(x => `${x.name}(${x.inputs.map(y=>y.type).join(',')})`), // map to $name($paramType1,$paramType2...) 91 | 92 | /** 93 | * Generates an erc165 compatible interface id from an array of method signatures. 94 | * Adapted from openzeppelin-solidity which we cannot import due to ES6 module issues 95 | * https://github.com/ldub/openzeppelin-solidity/blob/master/test/helpers/makeInterfaceId.js 96 | */ 97 | generateInterfaceId : (methodSignatures = []) => 98 | "0x" + methodSignatures 99 | .map(methodSignature => web3.sha3(methodSignature)) // keccak256 100 | .map(h => Buffer 101 | .from(h.substring(2), 'hex') 102 | .slice(0, 4) // bytes4() 103 | ) 104 | .reduce((memo, bytes) => { 105 | for (let i = 0; i < 4; i++) { 106 | memo[i] = memo[i] ^ bytes[i]; // xor 107 | } 108 | return memo; 109 | }, Buffer.alloc(4)) 110 | .toString('hex') 111 | }); 112 | 113 | module.exports = { 114 | STRINGS, AGENT_STATE, JOB_STATE, TX_STATUS, HELPERS 115 | }; 116 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | let HDWalletProvider = require("@truffle/hdwallet-provider"); 2 | let Web3 = require("web3"); 3 | 4 | let provider = (endpoint) => { 5 | if (process.env.HDWALLET_MNEMONIC) { 6 | return new HDWalletProvider(process.env.HDWALLET_MNEMONIC, endpoint); 7 | } else { 8 | return new Web3.providers.HttpProvider(endpoint); 9 | } 10 | } 11 | 12 | let truffleOptions = { 13 | networks: { 14 | local: { 15 | host: "127.0.0.1", 16 | port: 8545, 17 | network_id: "*" // Any network ID 18 | }, 19 | develop : { 20 | host: "127.0.0.1", 21 | port: 9545, 22 | network_id: "*", 23 | }, 24 | localhd: { 25 | provider: () => provider("http://127.0.0.1:8545"), 26 | network_id: "*" // Any network ID 27 | }, 28 | kovan: { 29 | gasPrice: 50000000000, 30 | provider: () => provider("https://kovan.infura.io/v3/" + process.env.InfuraKey), 31 | network_id: "42" // Kovan network ID 32 | }, 33 | ropsten: { 34 | gasPrice: 50000000000, 35 | provider: () => provider("https://ropsten.infura.io/v3/" + process.env.InfuraKey), 36 | network_id: "3", // ropsten network ID, 37 | }, 38 | main: { 39 | gasPrice: 50000000000, 40 | provider: () => provider("https://mainnet.infura.io/v3/" + process.env.InfuraKey), 41 | network_id: "1" // mainnet network ID 42 | }, 43 | }, 44 | mocha: { 45 | enableTimeouts: false, 46 | reporter: 'eth-gas-reporter', 47 | reporterOptions : { 48 | currency: 'USD', 49 | onlyCalledMethods: 'true', 50 | showTimeSpent: 'true' 51 | } 52 | }, 53 | // Configure your compilers 54 | compilers: { 55 | solc: { 56 | version: "0.6.2", // Fetch exact version from solc-bin (default: truffle's version) 57 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 58 | settings: { // See the solidity docs for advice about optimization and evmVersion 59 | optimizer: { 60 | enabled: true, 61 | //runs: 200 62 | }, 63 | //evmVersion: "byzantium" 64 | } 65 | }, 66 | }, 67 | // solc: { 68 | // optimizer: { 69 | // enabled: true, 70 | // runs: 200 71 | // } 72 | // } 73 | }; 74 | 75 | let reporterArg = process.argv.indexOf('--reporter'); 76 | if (reporterArg >= 0) { 77 | truffleOptions['mocha'] = { 78 | reporter: process.argv[reporterArg + 1] 79 | }; 80 | } 81 | 82 | module.exports = truffleOptions; -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singnet/batch-token-transfer/2b0aee683d3383b97e3485ffddcd54bc498d82be/truffle.js --------------------------------------------------------------------------------