├── .babelrc
├── .gitignore
├── .eslintignore
├── migrations
├── 2_deploy_contracts.js
└── 1_initial_migration.js
├── .eslintrc
├── truffle.js
├── test
├── TestToken.sol
├── truffle-keys.js
└── integration.test.js
├── contracts
├── Migrations.sol
└── Token.sol
├── webpack.config.js
├── LICENSE
├── package.json
├── app
├── css
│ └── narrow-jumbotron.css
├── pubnub-functions
│ └── sms-handler.js
├── index.html
└── js
│ └── app.js
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "es2015", "stage-0"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | package-lock.json
4 | build
5 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | tmp/**
2 | build/**
3 | node_modules/**
4 | contracts/**
5 | migrations/1_initial_migration.js
6 | migrations/2_deploy_contracts.js
7 | test/
8 |
--------------------------------------------------------------------------------
/migrations/2_deploy_contracts.js:
--------------------------------------------------------------------------------
1 | var Token = artifacts.require("./Token.sol");
2 |
3 | module.exports = function(deployer) {
4 | deployer.deploy(Token);
5 | };
6 |
--------------------------------------------------------------------------------
/migrations/1_initial_migration.js:
--------------------------------------------------------------------------------
1 | var Migrations = artifacts.require("./Migrations.sol");
2 |
3 | module.exports = function(deployer) {
4 | deployer.deploy(Migrations);
5 | };
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "standard"
5 | ],
6 | "plugins": [
7 | "babel"
8 | ],
9 | "rules": {
10 | "key-spacing" : 0,
11 | "jsx-quotes" : [2, "prefer-single"],
12 | "max-len" : [2, 120, 2],
13 | "object-curly-spacing" : [2, "always"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/truffle.js:
--------------------------------------------------------------------------------
1 | const mnemonic = process.env.ethereum_mnemonic;
2 | const HDWalletProvider = require("truffle-hdwallet-provider");
3 |
4 | require('babel-register');
5 | require('babel-polyfill');
6 |
7 | module.exports = {
8 | build: "npm run dev",
9 | networks: {
10 | development: {
11 | host: "127.0.0.1",
12 | port: 9545,
13 | network_id: "*" // Match any network id
14 | },
15 | ropsten: {
16 | provider: new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"),
17 | network_id: 3
18 | }
19 | }
20 | };
--------------------------------------------------------------------------------
/test/TestToken.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.4.18;
2 |
3 | import "truffle/Assert.sol";
4 | import "truffle/DeployedAddresses.sol";
5 | import "../contracts/Token.sol";
6 |
7 | contract TestToken {
8 |
9 | function testInitialBalanceUsingDeployedContract() public {
10 | Token tok = Token(DeployedAddresses.Token());
11 |
12 | // 1*10^27 wei
13 | uint expected = 1000000000000000000000000000;
14 | string memory details = 'Owner should have 1e+27 Token initially';
15 |
16 | Assert.equal(tok.balanceOf(tx.origin), expected, details);
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/contracts/Migrations.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.4.17;
2 |
3 | contract Migrations {
4 | address public owner;
5 | uint public last_completed_migration;
6 |
7 | modifier restricted() {
8 | if (msg.sender == owner) _;
9 | }
10 |
11 | function Migrations() public {
12 | owner = msg.sender;
13 | }
14 |
15 | function setCompleted(uint completed) public restricted {
16 | last_completed_migration = completed;
17 | }
18 |
19 | function upgrade(address new_address) public restricted {
20 | Migrations upgraded = Migrations(new_address);
21 | upgraded.setCompleted(last_completed_migration);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './app/js/app.js',
6 | output: {
7 | path: path.resolve(__dirname, 'build'),
8 | filename: 'app.js'
9 | },
10 | plugins: [
11 | // Copy our app's index.html to the build folder.
12 | new CopyWebpackPlugin([
13 | { from: './app/index.html', to: 'index.html' },
14 | { from: './app/css/', to: 'css/' }
15 | ])
16 | ],
17 | module: {
18 | rules: [
19 | {
20 | test: /\.css$/,
21 | use: [ 'style-loader', 'css-loader' ]
22 | }
23 | ],
24 | loaders: [
25 | { test: /\.json?$/, loader: 'babel', },
26 | {
27 | test: /\.js$/,
28 | exclude: /(node_modules|bower_components)/,
29 | loader: 'babel-loader',
30 | query: {
31 | presets: ['es2015'],
32 | plugins: ['transform-runtime']
33 | }
34 | }
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Adam Bavosa
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crowdsalable-eth-token",
3 | "version": "0.0.0",
4 | "description": "Crowdsalable Ethereum token",
5 | "license": "MIT",
6 | "scripts": {
7 | "lint": "eslint ./",
8 | "build": "webpack",
9 | "dev": "webpack-dev-server"
10 | },
11 | "devDependencies": {
12 | "babel-cli": "^6.22.2",
13 | "babel-core": "^6.22.1",
14 | "babel-eslint": "^6.1.2",
15 | "babel-loader": "^6.2.10",
16 | "babel-plugin-transform-runtime": "^6.23.0",
17 | "babel-preset-env": "^1.1.8",
18 | "babel-preset-es2015": "^6.24.1",
19 | "babel-register": "^6.22.0",
20 | "copy-webpack-plugin": "^4.0.1",
21 | "css-loader": "^0.26.1",
22 | "eslint": "^3.14.0",
23 | "eslint-config-standard": "^6.0.0",
24 | "eslint-plugin-babel": "^4.0.0",
25 | "eslint-plugin-mocha": "^4.8.0",
26 | "eslint-plugin-promise": "^3.0.0",
27 | "eslint-plugin-standard": "^2.0.0",
28 | "html-webpack-plugin": "^2.28.0",
29 | "json-loader": "^0.5.4",
30 | "style-loader": "^0.13.1",
31 | "truffle-contract": "^1.1.11",
32 | "truffle-hdwallet-provider": "0.0.3",
33 | "web3": "^0.20.0",
34 | "webpack": "^2.2.1",
35 | "webpack-dev-server": "^2.3.0",
36 | "babel-polyfill": "^6.26.0",
37 | "babel-preset-stage-0": "^6.24.1",
38 | "babel-runtime": "^6.26.0",
39 | "ethereumjs-tx": "^1.3.3",
40 | "zeppelin-solidity": "^1.6.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/truffle-keys.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Ethereum keysets used in test blockchain by `truffle develop`
3 | * DO NOT USE THESE KEYS ON THE MAIN NETWORK FOR ANY REASON
4 | */
5 |
6 | module.exports = {
7 | 'public': [
8 | '0x627306090abab3a6e1400e9345bc60c78a8bef57',
9 | '0xf17f52151ebef6c7334fad080c5704d77216b732',
10 | '0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef',
11 | '0x821aea9a577a9b44299b9c15c88cf3087f3b5544',
12 | '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2',
13 | '0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e',
14 | '0x2191ef87e392377ec08e7c08eb105ef5448eced5',
15 | '0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5',
16 | '0x6330a553fc93768f612722bb8c2ec78ac90b3bbc',
17 | '0x5aeda56215b167893e80b4fe645ba6d5bab767de',
18 | ],
19 | 'private': [
20 | 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3',
21 | 'ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f',
22 | '0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1',
23 | 'c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71f772aa6c',
24 | '388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418',
25 | '659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63',
26 | '82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8',
27 | 'aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7',
28 | '0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4',
29 | '8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5',
30 | ]
31 | };
32 |
--------------------------------------------------------------------------------
/app/css/narrow-jumbotron.css:
--------------------------------------------------------------------------------
1 | /* Space out content a bit */
2 | body {
3 | padding-top: 1.5rem;
4 | padding-bottom: 1.5rem;
5 | }
6 |
7 | /* Everything but the jumbotron gets side spacing for mobile first views */
8 | .header,
9 | .marketing,
10 | .footer {
11 | padding-right: 1rem;
12 | padding-left: 1rem;
13 | }
14 |
15 | /* Custom page header */
16 | .header {
17 | padding-bottom: 1rem;
18 | border-bottom: .05rem solid #e5e5e5;
19 | }
20 | /* Make the masthead heading the same height as the navigation */
21 | .header h3 {
22 | margin-top: 0;
23 | margin-bottom: 0;
24 | line-height: 3rem;
25 | }
26 |
27 | /* Custom page footer */
28 | .footer {
29 | padding-top: 1.5rem;
30 | color: #777;
31 | border-top: .05rem solid #e5e5e5;
32 | }
33 |
34 | /* Customize container */
35 | @media (min-width: 48em) {
36 | .container {
37 | max-width: 46rem;
38 | }
39 | }
40 | .container-narrow > hr {
41 | margin: 2rem 0;
42 | }
43 |
44 | /* Main marketing message and sign up button */
45 | .jumbotron {
46 | text-align: center;
47 | border-bottom: .05rem solid #e5e5e5;
48 | }
49 | .jumbotron .btn {
50 | padding: .75rem 1.5rem;
51 | font-size: 1.5rem;
52 | }
53 |
54 | /* Supporting marketing content */
55 | .marketing {
56 | margin: 3rem 0;
57 | }
58 | .marketing p + h4 {
59 | margin-top: 1.5rem;
60 | }
61 |
62 | /* Responsive: Portrait tablets and up */
63 | @media screen and (min-width: 48em) {
64 | /* Remove the padding we set earlier */
65 | .header,
66 | .marketing,
67 | .footer {
68 | padding-right: 0;
69 | padding-left: 0;
70 | }
71 | /* Space out the masthead */
72 | .header {
73 | margin-bottom: 2rem;
74 | }
75 | /* Remove the bottom border on the jumbotron for visual effect */
76 | .jumbotron {
77 | border-bottom: 0;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/pubnub-functions/sms-handler.js:
--------------------------------------------------------------------------------
1 | const xhr = require('xhr');
2 | const basicAuth = require('codec/auth');
3 |
4 | // Use the PubNub Functions Vault Module to store these keys securely.
5 | const username = '__YOUR_CLICKSEND_USER_NAME__';
6 | const authKey = '__YOUR_CLICKSEND_AUTH_KEY__';
7 | const uri = 'https://rest.clicksend.com/v3/sms/send';
8 | const authorization = basicAuth.basic(username, authKey);
9 |
10 | export default (request) => {
11 | let crowdsaleName = request.message.crowdsaleName;
12 | let contractAddress = request.message.contractAddress;
13 |
14 | // Ideally, the phone numbers would be in the PN request body or they could be
15 | // fetched from a secure endpoint right here using `return xhr.fetch().then()`
16 | // Never hard code phone numbers in function code.
17 | let crowdPhoneNumbers = ['+19999999999'];
18 | let body = `Crowdsale "${crowdsaleName}" for TOK is now open!` +
19 | ` Ethereum Address: ${contractAddress}`;
20 |
21 | let messages = [];
22 |
23 | // Create an object for each number that will be SMSed.
24 | // Object array will be the POST body to ClickSend.
25 | for (let number of crowdPhoneNumbers) {
26 | messages.push({
27 | source: 'pubnub-blocks',
28 | from: 'cstoken',
29 | body: body,
30 | to: number,
31 | custom_string: `TOK-${crowdsaleName}`
32 | });
33 | }
34 |
35 | // POST to ClickSend API
36 | return xhr.fetch(uri, {
37 | 'method' : 'POST',
38 | 'headers' : {
39 | 'Authorization' : authorization,
40 | 'Content-Type': 'application/json'
41 | },
42 | 'body': JSON.stringify({
43 | 'messages': messages
44 | }),
45 | 'timeout' : 5000
46 | })
47 | .then((res) => {
48 | return request.ok();
49 | })
50 | .catch((err) => {
51 | console.error(err);
52 | return request.abort();
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Ethereum Token
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Token Crowdsale
54 |
Launch a crowdsale. Inform your crowd in realtime with PubNub.
55 |
56 |
Launch
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
80 |
81 |
82 |
83 |
Transfer TOK
84 |
85 |
86 |
Anyone can trigger a transfer from any wallet on the test network.
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
See Wallet Balance
102 |
103 |
104 |
105 |
Balance
106 | ? TOK
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
Truffle Development Wallets
118 |
119 |
120 |
Number 1 is the contract owner
121 |
122 | - 0x627306090abab3a6e1400e9345bc60c78a8bef57
123 | - 0xf17f52151ebef6c7334fad080c5704d77216b732
124 | - 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
125 | - 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
126 | - 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
127 | - 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
128 | - 0x2191ef87e392377ec08e7c08eb105ef5448eced5
129 | - 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
130 | - 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
131 | - 0x5aeda56215b167893e80b4fe645ba6d5bab767de
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Crowdsalable Ethereum Token
2 |
3 | Launch an ERC-20 Ethereum token with crowdsale capability using the Truffle development kit.
4 |
5 | **Step-by-step tutorial for building this is available on the PubNub Blog, click here:**
6 |
7 | [](https://www.pubnub.com/blog/how-to-launch-your-own-crowdsalable-cryptocurrency-part-3/?devrel_gh=erc20-ethereum-token)
8 |
9 | Based on:
10 | - [ERC-20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md)
11 | - [Truffle](http://truffleframework.com/docs/)
12 | - [OpenZepplin](https://openzeppelin.org/)
13 | - [Web3.js](https://github.com/ethereum/web3.js/)
14 |
15 | ## Quick Start
16 | ```
17 | git clone git@github.com:ajb413/crowdsalable-eth-token.git
18 | cd crowdsalable-eth-token
19 | npm i
20 | truffle develop
21 |
22 | ## Truffle development blockchain and console are booted
23 | truffle(develop)> compile
24 | truffle(develop)> migrate
25 | truffle(develop)> test
26 | ```
27 |
28 | In a second command line window, navigate to the project directory.
29 | ```
30 | npm run dev
31 | Project is running at http://localhost:8080/
32 | ```
33 |
34 | Go to http://localhost:8080/ in a web browser and use the sample UI to check wallet balances and send sample tokens to wallets within your local machine's test network.
35 |
36 | ## Quick Start Explained
37 |
38 | First, the repository was pulled from GitHub to your local machine. Next, all of the npm packages in `package.json` were installed on your machine. Make sure you have Node.js 5.0 or later.
39 |
40 | Next we used the Truffle CLI to launch the Truffle development environment. That includes a `testrpc` instance. [Test RPC](https://github.com/trufflesuite/ganache-cli) is an instance of the Ethereum network that runs on your local machine. It starts with 10 random wallet key pairs that each have sufficient test ether. The instance that is started in the Truffle development console has the same 10 wallet addresses every time, which makes integration tests easier to write. Truffle develop hosts the instance at http://127.0.0.1:9545 by default.
41 |
42 | Next we compiled our [Solidity](http://solidity.readthedocs.io/en/develop/) code into ABI objects. Then we migrated, which means we deployed our smart contracts to the development blockchain. The contract we deployed is the crowdsalable token.
43 |
44 | When deployed, the entire balance of the tokens are issued to the first wallet in the list of 10 that Truffle boots with. This list can be found in `test/truffle-keys.js`. To edit the token properties like name, number, and symbol, check out the constructor in `contracts/Token.sol`.
45 |
46 | ```
47 | function Token() public {
48 | symbol = 'TOK';
49 | name = 'Token';
50 | decimals = 18;
51 | totalSupply = 1000000000 * 10**uint(decimals);
52 | balances[msg.sender] = totalSupply;
53 | Transfer(address(0), msg.sender, totalSupply);
54 | }
55 | ```
56 |
57 | Next we ran the JavaScript and Solidity tests, which are explained below.
58 |
59 | Next we opened another command line window, navigated to the project directory, and booted the test UI.
60 |
61 | The test UI can be accessed in a web browser. The test UI uses Web3.js, a JavaScript framework that communicates requests made from a web or node.js application to the Ethereum network. In this case, the `Web3.providers.HttpProvider` is your local machine.
62 |
63 | Requests can be made to the development blockchain to get balances of wallets and transfer token. The way Ethereum works is that you must spend ether if you are doing a blockchain altering operation, like transferring token. Reading data that is already written to the blockchain is free. So the `transfer` method costs some ether, and the `balanceOf` method is always free.
64 |
65 | An unrealistic circumstance of the test instance is that you can `transfer` to and from any wallet. On the real network this does not work because each `transfer` request must be signed by a user's private key in order to succeed.
66 |
67 | ## Testing
68 | If you are familiar with Javascript testing and Mocha, you will hit the ground running with [Truffle tests](http://truffleframework.com/docs/getting_started/testing). The Truffle development kit allows a developer to use [Javascript](http://truffleframework.com/docs/getting_started/javascript-tests) or [Solidity](http://truffleframework.com/docs/getting_started/solidity-tests) to run tests on your contracts.
69 |
70 | My tests demonstrate using both, but mostly JavaScript, because it is easier to define different senders using the development key pairs.
71 |
72 | The file `test/integration.test.js` demonstrates many scenarios for the Token methods and the crowdsale methods. There are examples for `.call()` and `web3.eth.sendRawTransaction()` using Web3.js. The raw transactions are very useful for sending a signed request from any wallet in which the user has the private and public key.
73 |
74 | The integration test file imports the sample key pairs booted by `truffle develop` from `test/truffle-keys.js`. Note that `truffle develop` has these keys hard coded somewhere in the npm package, not in this repo's test folder.
75 |
76 | The tests are run in the truffle console using just `test`, or from the command line using `truffle test` with an instance of `truffle develop` already running.
77 |
78 | ## Deploying
79 |
80 | Truffle can be used to deploy to any Ethereum network. There are several public test networks and of course the Main Ethereum Network. In the `truffle.js` config file, we can define connections to any network, and use them to deploy our contract using the Truffle CLI.
81 |
82 | The example uses an Ethereum wallet which has its unique mnemonic stored in the machine's environment variables as `ethereum_mnemonic`. The `ropsten` connection will deploy our contracts to the [Ropsten Test Network](https://ropsten.etherscan.io/) using a specified wallet. The Ropsten test network is consistently mined and test ether can be issued to your wallet instantly from a faucet.
83 |
84 | ```javascript
85 | const mnemonic = process.env.ethereum_mnemonic;
86 | const HDWalletProvider = require("truffle-hdwallet-provider");
87 |
88 | require('babel-register');
89 | require('babel-polyfill');
90 |
91 | module.exports = {
92 | build: "npm run dev",
93 | networks: {
94 | development: {
95 | host: "127.0.0.1",
96 | port: 9545,
97 | network_id: "*" // Match any network id
98 | },
99 | ropsten: {
100 | provider: new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"),
101 | network_id: 3
102 | }
103 | }
104 | };
105 | ```
106 |
107 | **Deploying to the Main Ethereum Network is costly and should only be done after you have extensively tested your contracts on test networks.**
108 |
109 | To deploy to Ropsten we can use the following command. Keep in mind that deploying costs ether on the network you are deploying to.
110 | ```
111 | truffle migrate --network ropsten
112 | ```
113 | You can easily write a connection to the main network in your config using the Ropsten example and the [Truffle docs](http://truffleframework.com/docs/advanced/configuration).
114 |
--------------------------------------------------------------------------------
/app/js/app.js:
--------------------------------------------------------------------------------
1 | import token_artifacts from '../../build/contracts/Token.json';
2 |
3 | var pubnub = new PubNub({
4 | publishKey : '__YOUR_PUBNUB_PUBLISH_KEY__',
5 | subscribeKey : '__YOUR_PUBNUB_SUBSCRIBE_KEY__'
6 | });
7 |
8 | var pubnubSMSChannel = '__YOUR_FUNCTION_LISTENING_CHANNEL__';
9 |
10 | var web3;
11 | var accounts;
12 | var owner;
13 | var contract;
14 | var truffleDevRpc = 'http://127.0.0.1:9545/';
15 | var truffleDevContractAddress = '0x345ca3e014aaf5dca488057592ee47305d9b3e10';
16 |
17 | window.App = {
18 | start: function() {
19 | contract = web3.eth.contract(token_artifacts.abi)
20 | .at(truffleDevContractAddress);
21 |
22 | // Get the initial accounts
23 | web3.eth.getAccounts(function(err, accs) {
24 | if (err !== null || accs.length === 0) {
25 | alert(
26 | 'There was an error fetching your accounts. ' +
27 | 'Make sure the Truffle Developer Ethereum client is running.'
28 | );
29 | return;
30 | }
31 |
32 | accounts = accs;
33 | owner = accounts[0];
34 | });
35 | },
36 |
37 | setTransferStatus: function(message) {
38 | transferStatus.innerText = message;
39 | },
40 |
41 | setCrowdsaleStatus: function(message) {
42 | crowdsaleStatus.innerText = message;
43 | },
44 |
45 | setBuyStatus: function(message) {
46 | buyStatus.innerText = message;
47 | },
48 |
49 | setSmsStatus: function(message) {
50 | smsStatus.innerText = message;
51 | },
52 |
53 | balanceOf: function() {
54 | var address = walletAddress.value;
55 | var walletWei = contract.balanceOf.call(address, function(error, balance) {
56 | if (error) {
57 | window.App.setTransferStatus('Error: ' + error);
58 | } else {
59 | // Balance is in wei. If your token doesn't have 18 decimal places,
60 | // you will need to write your own conversion.
61 | var walletToken = web3.fromWei(balance, 'ether');
62 | balanceLabel.innerText = walletToken.toString();
63 | }
64 | });
65 | },
66 |
67 | transfer: function() {
68 | // Go from ether to wei denomination. If your token doesn't have 18
69 | // decimal places, you will need to write your own conversion.
70 | var amountToken = transferAmount.value;
71 | var amount = web3.toWei(amountToken);
72 |
73 | var sender = transferFromAddress.value;
74 | var receiver = transferToAddress.value;
75 |
76 | window.App.setTransferStatus('Initiating transaction... (please wait)');
77 |
78 | contract.transfer(
79 | receiver,
80 | amount,
81 | {from: sender},
82 | function(error, transactionHash) {
83 | if (error) {
84 | window.App.setTransferStatus('Error: ' + error);
85 | } else {
86 | window.App.setTransferStatus('Transaction complete!');
87 | }
88 | });
89 | },
90 |
91 | buy: function() {
92 | var name = buyCrowdsaleName.value;
93 | var address = beneficiaryAddress.value;
94 | var amount = buyAmount.value;
95 |
96 | contract.crowdsalePurchase(
97 | name,
98 | address,
99 | {from: owner, value: amount},
100 | function(error, transactionHash) {
101 | if (error) {
102 | window.App.setBuyStatus('Error: ' + error);
103 | } else {
104 | window.App.setBuyStatus(
105 | 'Purchase complete! Check the wallet TOK balance'
106 | );
107 | }
108 | });
109 | },
110 |
111 | launchCrowdsale: function() {
112 | // See constraints for these in the Token Contract (contracts/Token.sol)
113 | var name = crowdsaleName.value;
114 | var open = true;
115 | var initialTokenSupply = web3.toWei(100000); // 100,000 TOK
116 | var exchangeRate = 3000000000000000000; // 3 TOK for 1 ETH
117 | var startTime = Math.floor(new Date().getTime() / 1000);
118 | var endTime = 0; // no end time
119 |
120 | contract.createCrowdsale(
121 | name,
122 | open,
123 | initialTokenSupply,
124 | exchangeRate,
125 | startTime,
126 | endTime,
127 | {
128 | from: owner,
129 | gas: 200000
130 | },
131 | function(error, transactionHash) {
132 | if (error) {
133 | console.error(error);
134 | window.App.setCrowdsaleStatus(
135 | 'Error during launch, see developer console.'
136 | );
137 | } else {
138 | window.App.setCrowdsaleStatus(
139 | 'The Crowdsale "' + name + '" is now live.'
140 | );
141 | window.App.smsCrowdOnNewCrowdsale(name, contract.address);
142 | }
143 | }
144 | );
145 | },
146 |
147 | // Sends text messages via PubNub FUNCTIONS
148 | // First deploy app/pubnub-functions/sms-handler.js
149 | // at admin.pubnub.com
150 | smsCrowdOnNewCrowdsale: function(name, address) {
151 | var publishConfig = {
152 | channel : pubnubSMSChannel,
153 | message : {
154 | 'crowdsaleName': name,
155 | 'contractAddress': address
156 | }
157 | }
158 |
159 | pubnub.publish(publishConfig, function(status, response) {
160 | console.log(status, response);
161 | if (status.error) {
162 | console.error(status.error);
163 | window.App.setSmsStatus(
164 | 'Error while texting the crowd, see developer console.'
165 | );
166 | } else {
167 | window.App.setSmsStatus(
168 | 'The crowd has been notified with SMS via ClickSend.'
169 | );
170 | }
171 | });
172 | }
173 | };
174 |
175 | window.addEventListener('load', function() {
176 | let providerURI = truffleDevRpc;
177 | let web3Provider = new Web3.providers.HttpProvider(providerURI);
178 | web3 = new Web3(web3Provider);
179 |
180 | App.start();
181 | });
182 |
183 | var walletAddress = document.getElementById('walletAddress');
184 | var balanceButton = document.getElementById('balanceButton');
185 | var balanceLabel = document.getElementById('Balance');
186 | var transferFromAddress = document.getElementById('transferFromAddress');
187 | var transferToAddress = document.getElementById('transferToAddress');
188 | var transferAmount = document.getElementById('transferAmount');
189 | var transferButton = document.getElementById('transferButton');
190 | var transferStatus = document.getElementById('transferStatus');
191 | var buyCrowdsaleName = document.getElementById('buyCrowdsaleName');
192 | var beneficiaryAddress = document.getElementById('beneficiaryAddress');
193 | var buyAmount = document.getElementById('buyAmount');
194 | var buyButton = document.getElementById('buyButton');
195 | var buyStatus = document.getElementById('buyStatus');
196 | var launchCrowdsaleButton = document.getElementById('launchButton');
197 | var crowdsaleStatus = document.getElementById('crowdsaleStatus');
198 | var crowdsaleName = document.getElementById('crowdsaleName');
199 | var smsStatus = document.getElementById('smsStatus');
200 |
201 | balanceButton.onclick = window.App.balanceOf;
202 | transferButton.onclick = window.App.transfer;
203 | buyButton.onclick = window.App.buy;
204 | launchCrowdsaleButton.onclick = window.App.launchCrowdsale;
205 |
--------------------------------------------------------------------------------
/contracts/Token.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.4.18;
2 |
3 | import 'zeppelin-solidity/contracts/math/SafeMath.sol';
4 | import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
5 |
6 | /**
7 | * Token
8 | *
9 | * @title A fixed supply ERC-20 token contract with crowdsale capability.
10 | * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
11 | */
12 | contract Token is Ownable {
13 | using SafeMath for uint;
14 | uint public constant MAX_UINT =
15 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
16 |
17 | struct Crowdsale {
18 | bool open;
19 | uint initialTokenSupply;
20 | uint tokenBalance;
21 | uint exchangeRate;
22 | uint startTime;
23 | uint endTime;
24 | }
25 |
26 | event CrowdsaleDeployed(
27 | string crowdsaleName,
28 | bool indexed open,
29 | uint initialTokenSupply,
30 | uint exchangeRate,
31 | uint indexed startTime,
32 | uint endTime
33 | );
34 |
35 | event TokenNameChanged(
36 | string previousName,
37 | string newName,
38 | uint indexed time
39 | );
40 |
41 | event TokenSymbolChanged(
42 | string previousSymbol,
43 | string newSymbol,
44 | uint indexed time
45 | );
46 |
47 | event Transfer(
48 | address indexed _from,
49 | address indexed _to,
50 | uint256 _value
51 | );
52 |
53 | event Approval(
54 | address indexed _owner,
55 | address indexed _spender,
56 | uint256 _value
57 | );
58 |
59 | string public symbol;
60 | string public name;
61 | uint8 public decimals;
62 | uint public totalSupply;
63 |
64 | mapping(address => uint) balances;
65 | mapping(address => mapping(address => uint)) allowed;
66 | mapping(string => Crowdsale) crowdsales;
67 |
68 | /**
69 | * Constructs the Token contract and gives all of the supply to the address
70 | * that deployed it. The fixed supply is 1 billion tokens with up to 18
71 | * decimal places.
72 | */
73 | function Token() public {
74 | symbol = 'TOK';
75 | name = 'Token';
76 | decimals = 18;
77 | totalSupply = 1000000000 * 10**uint(decimals);
78 | balances[msg.sender] = totalSupply;
79 | Transfer(address(0), msg.sender, totalSupply);
80 | }
81 |
82 | /**
83 | * @dev Fallback function
84 | */
85 | function() public payable { revert(); }
86 |
87 | /**
88 | * Gets the token balance of any wallet.
89 | * @param _owner Wallet address of the returned token balance.
90 | * @return The balance of tokens in the wallet.
91 | */
92 | function balanceOf(address _owner)
93 | public
94 | constant
95 | returns (uint balance)
96 | {
97 | return balances[_owner];
98 | }
99 |
100 | /**
101 | * Transfers tokens from the sender's wallet to the specified `_to` wallet.
102 | * @param _to Address of the transfer's recipient.
103 | * @param _value Number of tokens to transfer.
104 | * @return True if the transfer succeeded.
105 | */
106 | function transfer(address _to, uint _value) public returns (bool success) {
107 | balances[msg.sender] = balances[msg.sender].sub(_value);
108 | balances[_to] = balances[_to].add(_value);
109 | Transfer(msg.sender, _to, _value);
110 | return true;
111 | }
112 |
113 | /**
114 | * Transfer tokens from any wallet to the `_to` wallet. This only works if
115 | * the `_from` wallet has already allocated tokens for the caller keyset
116 | * using `approve`. From wallet must have sufficient balance to
117 | * transfer. Caller must have sufficient allowance to transfer.
118 | * @param _from Wallet address that tokens are withdrawn from.
119 | * @param _to Wallet address that tokens are deposited to.
120 | * @param _value Number of tokens transacted.
121 | * @return True if the transfer succeeded.
122 | */
123 | function transferFrom(address _from, address _to, uint _value)
124 | public
125 | returns (bool success)
126 | {
127 | balances[_from] = balances[_from].sub(_value);
128 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
129 | balances[_to] = balances[_to].add(_value);
130 | Transfer(_from, _to, _value);
131 | return true;
132 | }
133 |
134 | /**
135 | * Sender allows another wallet to `transferFrom` tokens from their wallet.
136 | * @param _spender Address of `transferFrom` recipient.
137 | * @param _value Number of tokens to `transferFrom`.
138 | * @return True if the approval succeeded.
139 | */
140 | function approve(address _spender, uint _value)
141 | public
142 | returns (bool success)
143 | {
144 | allowed[msg.sender][_spender] = _value;
145 | Approval(msg.sender, _spender, _value);
146 | return true;
147 | }
148 |
149 | /**
150 | * Gets the number of tokens that a `_owner` has approved for a _spender
151 | * to `transferFrom`.
152 | * @param _owner Wallet address that tokens can be withdrawn from.
153 | * @param _spender Wallet address that tokens can be deposited to.
154 | * @return The number of tokens allowed to be transferred.
155 | */
156 | function allowance(address _owner, address _spender)
157 | public
158 | constant
159 | returns (uint remaining)
160 | {
161 | return allowed[_owner][_spender];
162 | }
163 |
164 | /**
165 | * Changes the token's name. Does not check for improper characters.
166 | * @param newName String of the new name for the token.
167 | */
168 | function changeTokenName(string newName) public onlyOwner {
169 | require(bytes(newName).length > 0);
170 | string memory oldName = name;
171 | name = newName;
172 | TokenNameChanged(oldName, newName, now);
173 | }
174 |
175 | /**
176 | * Changes the token's symbol. Does not check for improper characters.
177 | * @param newSymbol String of the new symbol for the token.
178 | */
179 | function changeTokenSymbol(string newSymbol) public onlyOwner {
180 | require(bytes(newSymbol).length > 0);
181 | string memory oldSymbol = symbol;
182 | symbol = newSymbol;
183 | TokenSymbolChanged(oldSymbol, newSymbol, now);
184 | }
185 |
186 | /**
187 | * Creates a crowdsale. Tokens are withdrawn from the owner's account and
188 | * the balance is kept track of by the `tokenBalance` of the crowdsale.
189 | * A crowdsale can be opened or closed at any time by the owner using
190 | * the `openCrowdsale` and `closeCrowdsale` methods. The `open`,
191 | * `startTime`, and `endTime` properties are checked when a purchase is
192 | * attempted. Crowdsales permanently exist in the `crowdsales` map. If
193 | * the `endTime` for a crowdsale in the map is `0` the ncrowdsale does
194 | * not exist.
195 | * @param crowdsaleName String name of the crowdsale. Used as the
196 | * map key for a crowdsale struct instance in the `crowdsales` map.
197 | * A name can be used once and only once to initialize a crowdsale.
198 | * @param open Boolean of openness; can be changed at any time by the owner.
199 | * @param initialTokenSupply Number of tokens the crowdsale is deployed
200 | * with. This amount is a wei integer.
201 | * @param exchangeRate Token wei to Ethereum wei ratio.
202 | * @param startTime Unix epoch time in seconds for crowdsale start time.
203 | * @param endTime Unix epoch time in seconds for crowdsale end time. Any
204 | * uint256 can be used to set this however passing `0` will cause the
205 | * value to be set to the maximum uint256. If `0` is not passed, the
206 | * value must be greater than `startTime`.
207 | */
208 | function createCrowdsale(
209 | string crowdsaleName,
210 | bool open,
211 | uint initialTokenSupply,
212 | uint exchangeRate,
213 | uint startTime,
214 | uint endTime
215 | )
216 | public
217 | onlyOwner
218 | {
219 | require(
220 | initialTokenSupply > 0 && initialTokenSupply <= balances[owner]
221 | );
222 | require(bytes(crowdsaleName).length > 0);
223 | require(crowdsales[crowdsaleName].endTime == 0);
224 | require(exchangeRate > 0);
225 |
226 | if (endTime == 0) {
227 | endTime = MAX_UINT;
228 | }
229 |
230 | require(endTime > startTime);
231 |
232 | crowdsales[crowdsaleName] = Crowdsale({
233 | open: open,
234 | initialTokenSupply: initialTokenSupply,
235 | tokenBalance: initialTokenSupply,
236 | exchangeRate: exchangeRate,
237 | startTime: startTime,
238 | endTime: endTime
239 | });
240 |
241 | balances[owner] = balances[owner].sub(initialTokenSupply);
242 |
243 | CrowdsaleDeployed(
244 | crowdsaleName,
245 | open,
246 | initialTokenSupply,
247 | exchangeRate,
248 | startTime,
249 | endTime
250 | );
251 | }
252 |
253 | /**
254 | * Owner can change the crowdsale's `open` property to true at any time.
255 | * Only works on deployed crowdsales.
256 | * @param crowdsaleName String for the name of the crowdsale. Used as the
257 | * map key to find a crowdsale struct instance in the `crowdsales` map.
258 | * @return True if the open succeeded.
259 | */
260 | function openCrowdsale(string crowdsaleName)
261 | public
262 | onlyOwner
263 | returns (bool success)
264 | {
265 | require(crowdsales[crowdsaleName].endTime > 0);
266 | crowdsales[crowdsaleName].open = true;
267 | return true;
268 | }
269 |
270 | /**
271 | * Owner can change the crowdsale's `open` property to false at any time.
272 | * Only works on deployed crowdsales.
273 | * @param crowdsaleName String for the name of the crowdsale. Used as the
274 | * map key to find a crowdsale struct instance in the `crowdsales` map.
275 | * @return True if the close succeeded.
276 | */
277 | function closeCrowdsale(string crowdsaleName)
278 | public
279 | onlyOwner
280 | returns (bool success)
281 | {
282 | require(crowdsales[crowdsaleName].endTime > 0);
283 | crowdsales[crowdsaleName].open = false;
284 | return true;
285 | }
286 |
287 | /**
288 | * Owner can add tokens to the crowdsale after it is deployed. Owner must
289 | * have enough tokens in their balance for this method to succeed.
290 | * @param crowdsaleName String for the name of the crowdsale. Used as the
291 | * map key to find a crowdsale struct instance in the `crowdsales` map.
292 | * @param tokens Number of tokens to transfer from the owner to the
293 | * crowdsale's `tokenBalance` property.
294 | * @return True if the add succeeded.
295 | */
296 | function crowdsaleAddTokens(string crowdsaleName, uint tokens)
297 | public
298 | onlyOwner
299 | returns (bool success)
300 | {
301 | require(crowdsales[crowdsaleName].endTime > 0);
302 | require(balances[owner] >= tokens);
303 |
304 | balances[owner] = balances[owner].sub(tokens);
305 | crowdsales[crowdsaleName].tokenBalance =
306 | crowdsales[crowdsaleName].tokenBalance.add(tokens);
307 |
308 | Transfer(owner, address(this), tokens);
309 | return true;
310 | }
311 |
312 | /**
313 | * Owner can remove tokens from the crowdsale at any time. Crowdsale must
314 | * have enough tokens in its balance for this method to succeed.
315 | * @param crowdsaleName String for the name of the crowdsale. Used as the
316 | * map key to find a crowdsale struct instance in the `crowdsales` map.
317 | * @param tokens Number of tokens to transfer from the crowdsale
318 | * `tokenBalance` to the owner.
319 | * @return True if the remove succeeded.
320 | */
321 | function crowdsaleRemoveTokens(string crowdsaleName, uint tokens)
322 | public
323 | onlyOwner
324 | returns (bool success)
325 | {
326 | require(crowdsales[crowdsaleName].endTime > 0);
327 | require(crowdsales[crowdsaleName].tokenBalance >= tokens);
328 |
329 | balances[owner] = balances[owner].add(tokens);
330 | crowdsales[crowdsaleName].tokenBalance =
331 | crowdsales[crowdsaleName].tokenBalance.sub(tokens);
332 |
333 | Transfer(address(this), owner, tokens);
334 | return true;
335 | }
336 |
337 | /**
338 | * Owner can change the crowdsale's `exchangeRate` after it is deployed.
339 | * @param crowdsaleName String for the name of the crowdsale. Used as the
340 | * map key to find a crowdsale struct instance in the `crowdsales` map.
341 | * @param newExchangeRate Ratio of token wei to Ethereum wei for crowdsale
342 | * purchases.
343 | * @return True if the update succeeded.
344 | */
345 | function crowdsaleUpdateExchangeRate(
346 | string crowdsaleName,
347 | uint newExchangeRate
348 | )
349 | public
350 | onlyOwner
351 | returns (bool success)
352 | {
353 | // Only works on crowdsales that exist
354 | require(crowdsales[crowdsaleName].endTime > 0);
355 | crowdsales[crowdsaleName].exchangeRate = newExchangeRate;
356 | return true;
357 | }
358 |
359 | /**
360 | * Any wallet can purchase tokens using ether if the crowdsale is open. Note
361 | * that the math operations assume the operands are Ethereum wei and
362 | * Token wei.
363 | * @param crowdsaleName String for the name of the crowdsale. Used as the
364 | * map key to find a crowdsale struct instance in the `crowdsales` map.
365 | * @param beneficiary Address of the wallet that will receive the tokens from
366 | * the purchase. This can be any wallet address.
367 | * @return True if the purchase succeeded.
368 | */
369 | function crowdsalePurchase(
370 | string crowdsaleName,
371 | address beneficiary
372 | )
373 | public
374 | payable
375 | returns (bool success)
376 | {
377 | require(crowdsaleIsOpen(crowdsaleName));
378 |
379 | uint tokens = crowdsales[crowdsaleName].exchangeRate.mul(msg.value);
380 | require(crowdsales[crowdsaleName].tokenBalance >= tokens);
381 |
382 | crowdsales[crowdsaleName].tokenBalance =
383 | crowdsales[crowdsaleName].tokenBalance.sub(tokens);
384 | balances[beneficiary] = balances[beneficiary].add(tokens);
385 |
386 | Transfer(address(this), beneficiary, tokens);
387 | return true;
388 | }
389 |
390 | /**
391 | * Gets all the details for a declared crowdsale. If the passed name is not
392 | * associated with an existing crowdsale, the call errors.
393 | * @param crowdsaleName String for the name of the crowdsale. Used as the
394 | * map key to find a crowdsale struct instance in the `crowdsales` map.
395 | * @return Each member of a declared crowdsale struct.
396 | */
397 | function getCrowdsaleDetails(string crowdsaleName)
398 | public
399 | view
400 | returns
401 | (
402 | string name_,
403 | bool open,
404 | uint initialTokenSupply,
405 | uint tokenBalance,
406 | uint exchangeRate,
407 | uint startTime,
408 | uint endTime
409 | )
410 | {
411 | require(crowdsales[crowdsaleName].endTime > 0);
412 | return (
413 | crowdsaleName,
414 | crowdsales[crowdsaleName].open,
415 | crowdsales[crowdsaleName].initialTokenSupply,
416 | crowdsales[crowdsaleName].tokenBalance,
417 | crowdsales[crowdsaleName].exchangeRate,
418 | crowdsales[crowdsaleName].startTime,
419 | crowdsales[crowdsaleName].endTime
420 | );
421 | }
422 |
423 | /**
424 | * Gets the number of tokens the crowdsale has not yet sold.
425 | * @param crowdsaleName String for the name of the crowdsale. Used as the
426 | * map key to find a crowdsale struct instance in the `crowdsales` map.
427 | * @return Total number of tokens the crowdsale has not yet sold.
428 | */
429 | function crowdsaleTokenBalance(string crowdsaleName)
430 | public
431 | view
432 | returns (uint)
433 | {
434 | require(crowdsales[crowdsaleName].endTime > 0);
435 | return crowdsales[crowdsaleName].tokenBalance;
436 | }
437 |
438 | /**
439 | * Check if the crowdsale is open.
440 | * @param crowdsaleName String for the name of the crowdsale. Used as the
441 | * map key to find a crowdsale struct instance in the `crowdsales` map.
442 | * @return True if the crowdsale is open, false if it is closed.
443 | */
444 | function crowdsaleIsOpen(string crowdsaleName) public view returns (bool) {
445 | bool result = true;
446 |
447 | if (
448 | !crowdsales[crowdsaleName].open
449 | || crowdsales[crowdsaleName].startTime > now
450 | || crowdsales[crowdsaleName].endTime < now
451 | ) {
452 | result = false;
453 | }
454 |
455 | return result;
456 | }
457 | }
458 |
--------------------------------------------------------------------------------
/test/integration.test.js:
--------------------------------------------------------------------------------
1 | const EthereumTx = require('ethereumjs-tx');
2 | const privateKeys = require('./truffle-keys').private;
3 | const publicKeys = require('./truffle-keys').public;
4 | const Token = artifacts.require('./Token.sol');
5 |
6 | contract('Token', function(accounts) {
7 | let contract;
8 | let owner;
9 | let web3Contract;
10 | let eventCounter = {};
11 |
12 | before(async () => {
13 | contract = await Token.deployed();
14 | web3Contract = web3.eth.contract(contract.abi).at(contract.address);
15 | owner = web3Contract._eth.coinbase;
16 | let other = publicKeys[1];
17 |
18 | if (publicKeys[0] !== owner || publicKeys[1] !== other) {
19 | throw new Error('Use `truffle develop` and /test/truffle-keys.js');
20 | }
21 |
22 | // Counts every event that solidity functions fire.
23 | // TODO: Confirm individual event contents in each test.
24 | contract.allEvents({}, (error, details) => {
25 | if (error) {
26 | console.error(error);
27 | } else {
28 | let count = eventCounter[details.event];
29 | eventCounter[details.event] = count ? count + 1 : 1;
30 | }
31 | });
32 | });
33 |
34 | it('should pass if contract is deployed', async function() {
35 | let name = await contract.name.call();
36 | assert.strictEqual(name, 'Token');
37 | });
38 |
39 | it('should return inital token wei balance of 1*10^27', async function() {
40 | let ownerBalance = await contract.balanceOf.call(owner);
41 | ownerBalance = ownerBalance.toString();
42 | assert.strictEqual(ownerBalance, '1e+27');
43 | });
44 |
45 | it('should properly [transfer] token', async function() {
46 | let recipient = publicKeys[1];
47 | let tokenWei = 1000000;
48 |
49 | await contract.transfer(recipient, tokenWei);
50 |
51 | let ownerBalance = await contract.balanceOf.call(owner);
52 | let recipientBalance = await contract.balanceOf.call(recipient);
53 |
54 | assert.strictEqual(ownerBalance.toString(), '9.99999999999999999999e+26');
55 | assert.strictEqual(recipientBalance.toNumber(), tokenWei);
56 | });
57 |
58 | it('should properly between non-owners [transfer] token', async function() {
59 | let sender = publicKeys[1];
60 | let senderPrivateKey = privateKeys[1];
61 | let recipient = publicKeys[2];
62 | let tokenWei = 500000;
63 |
64 | let data = web3Contract.transfer.getData(recipient, tokenWei);
65 |
66 | let result = await rawTransaction(
67 | sender,
68 | senderPrivateKey,
69 | contract.address,
70 | data,
71 | 0
72 | );
73 |
74 | let senderBalance = await contract.balanceOf.call(sender);
75 | let recipientBalance = await contract.balanceOf.call(recipient);
76 |
77 | assert.strictEqual(senderBalance.toNumber(), tokenWei);
78 | assert.strictEqual(recipientBalance.toNumber(), tokenWei);
79 | assert.strictEqual(0, result.indexOf('0x'));
80 | });
81 |
82 | it('should fail to [transfer] token too much token', async function() {
83 | let sender = publicKeys[1];
84 | let senderPrivateKey = privateKeys[1];
85 | let recipient = publicKeys[2];
86 | let tokenWei = 50000000;
87 |
88 | let data = web3Contract.transfer.getData(recipient, tokenWei);
89 |
90 | let errorMessage;
91 | try {
92 | await rawTransaction(
93 | sender,
94 | senderPrivateKey,
95 | contract.address,
96 | data,
97 | 0
98 | );
99 | } catch (error) {
100 | errorMessage = error.message;
101 | }
102 |
103 | assert.strictEqual(
104 | errorMessage,
105 | 'VM Exception while processing transaction: invalid opcode'
106 | );
107 |
108 | let senderBalance = await contract.balanceOf.call(sender);
109 | let recipientBalance = await contract.balanceOf.call(recipient);
110 |
111 | assert.strictEqual(senderBalance.toNumber(), 500000);
112 | assert.strictEqual(recipientBalance.toNumber(), 500000);
113 | });
114 |
115 | it('should properly return the [totalSupply] of tokens', async function() {
116 | let totalSupply = await contract.totalSupply.call();
117 | totalSupply = totalSupply.toString();
118 | assert.strictEqual(totalSupply, '1e+27');
119 | });
120 |
121 | it('should [approve] token for [transferFrom]', async function() {
122 | let approver = owner;
123 | let spender = publicKeys[2];
124 |
125 | let originalAllowance = await contract.allowance.call(approver, spender);
126 |
127 | let tokenWei = 5000000;
128 | await contract.approve(spender, tokenWei);
129 |
130 | let resultAllowance = await contract.allowance.call(approver, spender);
131 |
132 | assert.strictEqual(originalAllowance.toNumber(), 0);
133 | assert.strictEqual(resultAllowance.toNumber(), tokenWei);
134 | });
135 |
136 | it('should fail to [transferFrom] more than allowed', async function() {
137 | let from = owner;
138 | let to = publicKeys[2];
139 | let spenderPrivateKey = privateKeys[2];
140 | let tokenWei = 10000000;
141 |
142 | let allowance = await contract.allowance.call(from, to);
143 | let ownerBalance = await contract.balanceOf.call(from);
144 | let spenderBalance = await contract.balanceOf.call(to);
145 |
146 | let data = web3Contract.transferFrom.getData(from, to, tokenWei);
147 |
148 | let errorMessage;
149 | try {
150 | await rawTransaction(
151 | to,
152 | spenderPrivateKey,
153 | contract.address,
154 | data,
155 | 0
156 | );
157 | } catch (error) {
158 | errorMessage = error.message;
159 | }
160 |
161 | assert.strictEqual(
162 | errorMessage,
163 | 'VM Exception while processing transaction: invalid opcode'
164 | );
165 | });
166 |
167 | it('should [transferFrom] approved tokens', async function() {
168 | let from = owner;
169 | let to = publicKeys[2];
170 | let spenderPrivateKey = privateKeys[2];
171 | let tokenWei = 5000000;
172 |
173 | let allowance = await contract.allowance.call(from, to);
174 | let ownerBalance = await contract.balanceOf.call(from);
175 | let spenderBalance = await contract.balanceOf.call(to);
176 |
177 | let data = web3Contract.transferFrom.getData(from, to, tokenWei);
178 |
179 | let result = await rawTransaction(
180 | to,
181 | spenderPrivateKey,
182 | contract.address,
183 | data,
184 | 0
185 | );
186 |
187 | let allowanceAfter = await contract.allowance.call(from, to);
188 | let ownerBalanceAfter = await contract.balanceOf.call(from);
189 | let spenderBalanceAfter = await contract.balanceOf.call(to);
190 |
191 | // Correct account balances
192 | // toString() numbers that are too large for js
193 | assert.strictEqual(
194 | ownerBalance.toString(),
195 | ownerBalanceAfter.add(tokenWei).toString()
196 | );
197 | assert.strictEqual(
198 | spenderBalance.add(tokenWei).toString(),
199 | spenderBalanceAfter.toString()
200 | );
201 |
202 | // Proper original allowance
203 | assert.strictEqual(allowance.toNumber(), tokenWei);
204 |
205 | // All of the allowance should have been used
206 | assert.strictEqual(allowanceAfter.toNumber(), 0);
207 |
208 | // Normal transaction hash, not an error.
209 | assert.strictEqual(0, result.indexOf('0x'));
210 | });
211 |
212 | it('should fail [changeTokenName] for non-owner', async function() {
213 | let notOwner = publicKeys[2];
214 | let notOwnerPrivateKey = privateKeys[2];
215 |
216 | let data = web3Contract.changeTokenName.getData('NewName');
217 |
218 | let errorMessage;
219 | try {
220 | await rawTransaction(
221 | notOwner,
222 | notOwnerPrivateKey,
223 | contract.address,
224 | data,
225 | 0
226 | );
227 | } catch (error) {
228 | errorMessage = error.message;
229 | }
230 |
231 | let expected = 'VM Exception while processing transaction: revert';
232 | assert.strictEqual(errorMessage, expected);
233 | });
234 |
235 | it('should properly [changeTokenName] by the owner', async function() {
236 | let ownerPrivateKey = privateKeys[0];
237 | let oldName = await contract.name.call();
238 |
239 | // attempt to `changeTokenName`
240 | let data = web3Contract.changeTokenName.getData('NewName');
241 |
242 | let result = await rawTransaction(
243 | owner,
244 | ownerPrivateKey,
245 | contract.address,
246 | data,
247 | 0
248 | );
249 |
250 | let newName = await contract.name.call();
251 |
252 | assert.strictEqual(oldName, 'Token');
253 | assert.strictEqual(newName, 'NewName');
254 | assert.strictEqual(0, result.indexOf('0x'));
255 | });
256 |
257 | it('should fail [changeTokenSymbol] for not the owner', async function() {
258 | let notOwner = publicKeys[3];
259 | let notOwnerPrivateKey = privateKeys[3];
260 |
261 | let data = web3Contract.changeTokenSymbol.getData('XYZ');
262 |
263 | let errorMessage;
264 | try {
265 | await rawTransaction(
266 | notOwner,
267 | notOwnerPrivateKey,
268 | contract.address,
269 | data,
270 | 0
271 | );
272 | } catch (error) {
273 | errorMessage = error.message;
274 | }
275 |
276 | let expected = 'VM Exception while processing transaction: revert';
277 | assert.strictEqual(errorMessage, expected);
278 | });
279 |
280 | it('should properly [changeTokenSymbol] by the owner', async function() {
281 | let ownerPrivateKey = privateKeys[0];
282 | let oldSymbol = await contract.symbol.call();
283 |
284 | // attempt to `changeTokenName`
285 | let data = web3Contract.changeTokenSymbol.getData('ABC');
286 |
287 | let result = await rawTransaction(
288 | owner,
289 | ownerPrivateKey,
290 | contract.address,
291 | data,
292 | 0
293 | );
294 |
295 | let newSymbol = await contract.symbol.call();
296 |
297 | assert.strictEqual(oldSymbol, 'TOK');
298 | assert.strictEqual(newSymbol, 'ABC');
299 | assert.strictEqual(0, result.indexOf('0x'));
300 | });
301 |
302 | it('should properly [createCrowdsale] for owner', async function() {
303 | let ownerPrivateKey = privateKeys[0];
304 |
305 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
306 |
307 | // attempt to `createCrowdsale` that is open and happening now
308 | let data = web3Contract.createCrowdsale.getData(
309 | 'crowdsale1', /* name */
310 | true, /* open */
311 | 50000, /* initialTokenSupply */
312 | 400, /* exchangeRate */
313 | Math.floor(new Date().getTime() / 1000 - 5), /* startTime */
314 | Math.floor(new Date().getTime() / 1000 + 1000), /* endTime */
315 | );
316 |
317 | let result = await rawTransaction(
318 | owner,
319 | ownerPrivateKey,
320 | contract.address,
321 | data,
322 | 0
323 | );
324 |
325 | let openAfter = await contract.crowdsaleIsOpen.call('crowdsale1');
326 |
327 | assert.strictEqual(open, false);
328 | assert.strictEqual(openAfter, true);
329 | assert.strictEqual(0, result.indexOf('0x'));
330 | });
331 |
332 | it('should [createCrowdsale] when passing 0 endTime', async function() {
333 | let ownerPrivateKey = privateKeys[0];
334 |
335 | let open = await contract.crowdsaleIsOpen.call('crowdsale2');
336 |
337 | // attempt to `createCrowdsale` with a max int end time
338 | let data = web3Contract.createCrowdsale.getData(
339 | 'crowdsale2', /* name */
340 | true, /* open */
341 | 50000, /* initialTokenSupply */
342 | 400, /* exchangeRate */
343 | Math.floor(new Date().getTime() / 1000 - 5), /* startTime */
344 | 0, /* endTime */
345 | );
346 |
347 | let result = await rawTransaction(
348 | owner,
349 | ownerPrivateKey,
350 | contract.address,
351 | data,
352 | 0
353 | );
354 |
355 | let openAfter = await contract.crowdsaleIsOpen.call('crowdsale1');
356 |
357 | assert.strictEqual(open, false);
358 | assert.strictEqual(openAfter, true);
359 | assert.strictEqual(0, result.indexOf('0x'));
360 | });
361 |
362 | it('should fail to [createCrowdsale] with existing name', async function() {
363 | let ownerPrivateKey = privateKeys[0];
364 |
365 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
366 |
367 | // attempt to `createCrowdsale` that is already existing
368 | let data = web3Contract.createCrowdsale.getData(
369 | 'crowdsale1', /* name */
370 | true, /* open */
371 | 50000, /* initialTokenSupply */
372 | 400, /* exchangeRate */
373 | Math.floor(new Date().getTime() / 1000 - 5), /* startTime */
374 | Math.floor(new Date().getTime() / 1000 + 1000), /* endTime */
375 | );
376 |
377 | let errorMessage;
378 | try {
379 | await rawTransaction(
380 | owner,
381 | ownerPrivateKey,
382 | contract.address,
383 | data,
384 | 0
385 | );
386 | } catch (error) {
387 | errorMessage = error.message;
388 | }
389 |
390 | let expected = 'VM Exception while processing transaction: revert';
391 | assert.strictEqual(errorMessage, expected);
392 | });
393 |
394 | it('should fail to [createCrowdsale] bad exchangeRate', async function() {
395 | let ownerPrivateKey = privateKeys[0];
396 |
397 | let data = web3Contract.createCrowdsale.getData(
398 | 'crowdsale3', /* name */
399 | true, /* open */
400 | 50000, /* initialTokenSupply */
401 | 0, /* exchangeRate */
402 | Math.floor(new Date().getTime() / 1000 - 5), /* startTime */
403 | Math.floor(new Date().getTime() / 1000 + 1000), /* endTime */
404 | );
405 |
406 | let errorMessage;
407 | try {
408 | await rawTransaction(
409 | owner,
410 | ownerPrivateKey,
411 | contract.address,
412 | data,
413 | 0
414 | );
415 | } catch (error) {
416 | errorMessage = error.message;
417 | }
418 |
419 | let expected = 'VM Exception while processing transaction: revert';
420 | assert.strictEqual(errorMessage, expected);
421 | });
422 |
423 | it('should fail to [createCrowdsale] bad dates', async function() {
424 | let ownerPrivateKey = privateKeys[0];
425 |
426 | let data = web3Contract.createCrowdsale.getData(
427 | 'crowdsale3', /* name */
428 | true, /* open */
429 | 50000, /* initialTokenSupply */
430 | 123, /* exchangeRate */
431 | Math.floor(new Date().getTime() / 1000 + 1000), /* startTime */
432 | Math.floor(new Date().getTime() / 1000 - 5) /* endTime */
433 | );
434 |
435 | let errorMessage;
436 | try {
437 | await rawTransaction(
438 | owner,
439 | ownerPrivateKey,
440 | contract.address,
441 | data,
442 | 0
443 | );
444 | } catch (error) {
445 | errorMessage = error.message;
446 | }
447 |
448 | let expected = 'VM Exception while processing transaction: revert';
449 | assert.strictEqual(errorMessage, expected);
450 | });
451 |
452 | it('[crowdsaleIsOpen] should fail for non-existing cs', async function() {
453 | let notOwner = publicKeys[1];
454 | let notOwnerPrivateKey = privateKeys[1];
455 |
456 | let open = await contract.crowdsaleIsOpen.call('crowdsale3');
457 |
458 | assert.strictEqual(open, false);
459 | });
460 |
461 | it('should fail to [createCrowdsale] bad name', async function() {
462 | let ownerPrivateKey = privateKeys[0];
463 |
464 | let data = web3Contract.createCrowdsale.getData(
465 | '', /* name */
466 | true, /* open */
467 | 50000, /* initialTokenSupply */
468 | 123, /* exchangeRate */
469 | Math.floor(new Date().getTime() / 1000 - 5), /* startTime */
470 | Math.floor(new Date().getTime() / 1000 + 1000) /* endTime */
471 | );
472 |
473 | let errorMessage;
474 | try {
475 | await rawTransaction(
476 | owner,
477 | ownerPrivateKey,
478 | contract.address,
479 | data,
480 | 0
481 | );
482 | } catch (error) {
483 | errorMessage = error.message;
484 | }
485 |
486 | let expected = 'VM Exception while processing transaction: revert';
487 | assert.strictEqual(errorMessage, expected);
488 | });
489 |
490 | it('[crowdsaleIsOpen] should return true for open cs', async function() {
491 | let notOwner = publicKeys[2];
492 | let notOwnerPrivateKey = privateKeys[2];
493 |
494 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
495 |
496 | assert.strictEqual(open, true);
497 | });
498 |
499 | it('should fail to [closeCrowdsale] for non-owner', async function() {
500 | let notOwner = publicKeys[2];
501 | let notOwnerPrivateKey = privateKeys[2];
502 |
503 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
504 |
505 | // attempt to close the crowdsale
506 | let data = web3Contract.closeCrowdsale.getData('crowdsale1');
507 |
508 | let errorMessage;
509 | try {
510 | await rawTransaction(
511 | notOwner,
512 | notOwnerPrivateKey,
513 | contract.address,
514 | data,
515 | 0
516 | );
517 | } catch (error) {
518 | errorMessage = error.message;
519 | }
520 |
521 | let openAfter = await contract.crowdsaleIsOpen.call('crowdsale1');
522 |
523 | assert.strictEqual(open, true);
524 | assert.strictEqual(openAfter, true);
525 |
526 | let expected = 'VM Exception while processing transaction: revert';
527 | assert.strictEqual(errorMessage, expected);
528 | });
529 |
530 | it('should [closeCrowdsale] for owner only', async function() {
531 | let ownerPrivateKey = privateKeys[0];
532 |
533 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
534 |
535 | // attempt to close the crowdsale
536 | let data = web3Contract.closeCrowdsale.getData('crowdsale1');
537 |
538 | let result = await rawTransaction(
539 | owner,
540 | ownerPrivateKey,
541 | contract.address,
542 | data,
543 | 0
544 | );
545 |
546 | let openAfter = await contract.crowdsaleIsOpen.call('crowdsale1');
547 |
548 | assert.strictEqual(open, true);
549 | assert.strictEqual(openAfter, false);
550 | assert.strictEqual(0, result.indexOf('0x'));
551 | });
552 |
553 | it('[crowdsaleIsOpen] should return false for closed cs', async function() {
554 | let notOwner = publicKeys[2];
555 | let notOwnerPrivateKey = privateKeys[2];
556 |
557 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
558 |
559 | assert.strictEqual(open, false);
560 | });
561 |
562 | it('should fail to [openCrowdsale] for non-owner', async function() {
563 | let notOwner = publicKeys[2];
564 | let notOwnerPrivateKey = privateKeys[2];
565 |
566 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
567 |
568 | let data = web3Contract.openCrowdsale.getData('crowdsale1');
569 |
570 | let errorMessage;
571 | try {
572 | await rawTransaction(
573 | notOwner,
574 | notOwnerPrivateKey,
575 | contract.address,
576 | data,
577 | 0
578 | );
579 | } catch (error) {
580 | errorMessage = error.message;
581 | }
582 |
583 | let openAfter = await contract.crowdsaleIsOpen.call('crowdsale1');
584 |
585 | assert.strictEqual(open, false);
586 | assert.strictEqual(openAfter, false);
587 |
588 | let expected = 'VM Exception while processing transaction: revert';
589 | assert.strictEqual(errorMessage, expected);
590 | });
591 |
592 | it('should [openCrowdsale] for owner only', async function() {
593 | let ownerPrivateKey = privateKeys[0];
594 |
595 | let open = await contract.crowdsaleIsOpen.call('crowdsale1');
596 |
597 | let data = web3Contract.openCrowdsale.getData('crowdsale1');
598 |
599 | let result = await rawTransaction(
600 | owner,
601 | ownerPrivateKey,
602 | contract.address,
603 | data,
604 | 0
605 | );
606 |
607 | let openAfter = await contract.crowdsaleIsOpen.call('crowdsale1');
608 |
609 | assert.strictEqual(open, false);
610 | assert.strictEqual(openAfter, true);
611 | assert.strictEqual(0, result.indexOf('0x'));
612 | });
613 |
614 | it('should fail to [crowdsaleAddTokens] for non-owner', async function() {
615 | let notOwner = publicKeys[1];
616 | let notOwnerPrivateKey = privateKeys[1];
617 |
618 | let crowdsaleTokens =
619 | await contract.crowdsaleTokenBalance.call('crowdsale1');
620 | let ownerTokens = await contract.balanceOf.call(owner);
621 | let notOwnerTokens = await contract.balanceOf.call(notOwner);
622 |
623 | let data = web3Contract.crowdsaleAddTokens.getData(
624 | 'crowdsale1',
625 | web3.toBigNumber('100')
626 | );
627 |
628 | let errorMessage;
629 | try {
630 | await rawTransaction(
631 | notOwner,
632 | notOwnerPrivateKey,
633 | contract.address,
634 | data,
635 | 0
636 | );
637 | } catch (error) {
638 | errorMessage = error.message;
639 | }
640 |
641 | let crowdsaleTokensAfter =
642 | await contract.crowdsaleTokenBalance.call('crowdsale1');
643 | let ownerTokensAfter = await contract.balanceOf.call(owner);
644 | let notOwnerTokensAfter = await contract.balanceOf.call(notOwner);
645 |
646 | crowdsaleTokens = crowdsaleTokens.toString();
647 | ownerTokens = ownerTokens.toString();
648 | notOwnerTokens = notOwnerTokens.toString();
649 | crowdsaleTokensAfter = crowdsaleTokensAfter.toString();
650 | ownerTokensAfter = ownerTokensAfter.toString();
651 | notOwnerTokensAfter = notOwnerTokensAfter.toString();
652 |
653 | assert.strictEqual(crowdsaleTokens, crowdsaleTokensAfter);
654 | assert.strictEqual(ownerTokens, ownerTokensAfter);
655 | assert.strictEqual(notOwnerTokens, notOwnerTokensAfter);
656 |
657 | let expected = 'VM Exception while processing transaction: revert';
658 | assert.strictEqual(errorMessage, expected);
659 | });
660 |
661 | it('should fail to [crowdsaleAddTokens] too many tokens', async function() {
662 | let ownerPrivateKey = privateKeys[0];
663 |
664 | let crowdsaleTokens =
665 | await contract.crowdsaleTokenBalance.call('crowdsale1');
666 | let ownerTokens = await contract.balanceOf.call(owner);
667 |
668 | let data = web3Contract.crowdsaleAddTokens.getData(
669 | 'crowdsale1',
670 | web3.toBigNumber('1e+50')
671 | );
672 |
673 | let errorMessage;
674 | try {
675 | await rawTransaction(
676 | owner,
677 | ownerPrivateKey,
678 | contract.address,
679 | data,
680 | 0
681 | );
682 | } catch (error) {
683 | errorMessage = error.message;
684 | }
685 |
686 | let crowdsaleTokensAfter =
687 | await contract.crowdsaleTokenBalance.call('crowdsale1');
688 | let ownerTokensAfter = await contract.balanceOf.call(owner);
689 |
690 | crowdsaleTokens = crowdsaleTokens.toString();
691 | ownerTokens = ownerTokens.toString();
692 | crowdsaleTokensAfter = crowdsaleTokensAfter.toString();
693 | ownerTokensAfter = ownerTokensAfter.toString();
694 |
695 | assert.strictEqual(crowdsaleTokens, crowdsaleTokensAfter);
696 | assert.strictEqual(ownerTokens, ownerTokensAfter);
697 |
698 | let expected = 'VM Exception while processing transaction: revert';
699 | assert.strictEqual(errorMessage, expected);
700 | });
701 |
702 | it('should [crowdsaleAddTokens] for owner only', async function() {
703 | let ownerPrivateKey = privateKeys[0];
704 |
705 | let crowdsaleTokens =
706 | await contract.crowdsaleTokenBalance.call('crowdsale1');
707 | let ownerTokens = await contract.balanceOf.call(owner);
708 |
709 | let data = web3Contract.crowdsaleAddTokens.getData(
710 | 'crowdsale1',
711 | web3.toBigNumber('5000')
712 | );
713 |
714 | let result = await rawTransaction(
715 | owner,
716 | ownerPrivateKey,
717 | contract.address,
718 | data,
719 | 0
720 | );
721 |
722 | let crowdsaleTokensAfter =
723 | await contract.crowdsaleTokenBalance.call('crowdsale1');
724 | let ownerTokensAfter = await contract.balanceOf.call(owner);
725 |
726 | crowdsaleTokens = crowdsaleTokens.toString();
727 | ownerTokens = ownerTokens.toString();
728 | crowdsaleTokensAfter = crowdsaleTokensAfter.toString();
729 | ownerTokensAfter = ownerTokensAfter.toString();
730 |
731 | assert.strictEqual(crowdsaleTokens, '50000');
732 | assert.strictEqual(ownerTokens, '9.999999999999999999939e+26');
733 | assert.strictEqual(crowdsaleTokensAfter, '55000');
734 | assert.strictEqual(ownerTokensAfter, '9.99999999999999999993895e+26');
735 | assert.strictEqual(0, result.indexOf('0x'));
736 | });
737 |
738 | it('should fail to [crowdsaleRemoveTokens] for non-owner', async function() {
739 | let notOwner = publicKeys[1];
740 | let notOwnerPrivateKey = privateKeys[1];
741 |
742 | let crowdsaleTokens =
743 | await contract.crowdsaleTokenBalance.call('crowdsale1');
744 | let ownerTokens = await contract.balanceOf.call(owner);
745 | let notOwnerTokens = await contract.balanceOf.call(notOwner);
746 |
747 | let data = web3Contract.crowdsaleRemoveTokens.getData(
748 | 'crowdsale1',
749 | web3.toBigNumber('100')
750 | );
751 |
752 | let errorMessage;
753 | try {
754 | await rawTransaction(
755 | notOwner,
756 | notOwnerPrivateKey,
757 | contract.address,
758 | data,
759 | 0
760 | );
761 | } catch (error) {
762 | errorMessage = error.message;
763 | }
764 |
765 | let crowdsaleTokensAfter =
766 | await contract.crowdsaleTokenBalance.call('crowdsale1');
767 | let ownerTokensAfter = await contract.balanceOf.call(owner);
768 | let notOwnerTokensAfter = await contract.balanceOf.call(notOwner);
769 |
770 | crowdsaleTokens = crowdsaleTokens.toString();
771 | ownerTokens = ownerTokens.toString();
772 | notOwnerTokens = notOwnerTokens.toString();
773 | crowdsaleTokensAfter = crowdsaleTokensAfter.toString();
774 | ownerTokensAfter = ownerTokensAfter.toString();
775 | notOwnerTokensAfter = notOwnerTokensAfter.toString();
776 |
777 | assert.strictEqual(crowdsaleTokens, crowdsaleTokensAfter);
778 | assert.strictEqual(ownerTokens, ownerTokensAfter);
779 | assert.strictEqual(notOwnerTokens, notOwnerTokensAfter);
780 |
781 | let expected = 'VM Exception while processing transaction: revert';
782 | assert.strictEqual(errorMessage, expected);
783 | });
784 |
785 | it('should fail [crowdsaleRemoveTokens] too many tokens', async function() {
786 | let ownerPrivateKey = privateKeys[0];
787 |
788 | let crowdsaleTokens =
789 | await contract.crowdsaleTokenBalance.call('crowdsale1');
790 | let ownerTokens = await contract.balanceOf.call(owner);
791 |
792 | let data = web3Contract.crowdsaleRemoveTokens.getData(
793 | 'crowdsale1',
794 | web3.toBigNumber('1e+50')
795 | );
796 |
797 | let errorMessage;
798 | try {
799 | await rawTransaction(
800 | owner,
801 | ownerPrivateKey,
802 | contract.address,
803 | data,
804 | 0
805 | );
806 | } catch (error) {
807 | errorMessage = error.message;
808 | }
809 |
810 | let crowdsaleTokensAfter =
811 | await contract.crowdsaleTokenBalance.call('crowdsale1');
812 | let ownerTokensAfter = await contract.balanceOf.call(owner);
813 |
814 | crowdsaleTokens = crowdsaleTokens.toString();
815 | ownerTokens = ownerTokens.toString();
816 | crowdsaleTokensAfter = crowdsaleTokensAfter.toString();
817 | ownerTokensAfter = ownerTokensAfter.toString();
818 |
819 | assert.strictEqual(crowdsaleTokens, crowdsaleTokensAfter);
820 | assert.strictEqual(ownerTokens, ownerTokensAfter);
821 |
822 | let expected = 'VM Exception while processing transaction: revert';
823 | assert.strictEqual(errorMessage, expected);
824 | });
825 |
826 | it('should [crowdsaleRemoveTokens] for owner only', async function() {
827 | let ownerPrivateKey = privateKeys[0];
828 |
829 | let crowdsaleTokens =
830 | await contract.crowdsaleTokenBalance.call('crowdsale1');
831 | let ownerTokens = await contract.balanceOf.call(owner);
832 |
833 | let data = web3Contract.crowdsaleRemoveTokens.getData(
834 | 'crowdsale1',
835 | web3.toBigNumber('5000')
836 | );
837 |
838 | let result = await rawTransaction(
839 | owner,
840 | ownerPrivateKey,
841 | contract.address,
842 | data,
843 | 0
844 | );
845 |
846 | let crowdsaleTokensAfter =
847 | await contract.crowdsaleTokenBalance.call('crowdsale1');
848 | let ownerTokensAfter = await contract.balanceOf.call(owner);
849 |
850 | crowdsaleTokens = crowdsaleTokens.toString();
851 | ownerTokens = ownerTokens.toString();
852 | crowdsaleTokensAfter = crowdsaleTokensAfter.toString();
853 | ownerTokensAfter = ownerTokensAfter.toString();
854 |
855 | assert.strictEqual(crowdsaleTokens, '55000');
856 | assert.strictEqual(ownerTokens, '9.99999999999999999993895e+26');
857 | assert.strictEqual(crowdsaleTokensAfter, '50000');
858 | assert.strictEqual(ownerTokensAfter, '9.999999999999999999939e+26');
859 | assert.strictEqual(0, result.indexOf('0x'));
860 | });
861 |
862 | it('should fail [crowdsaleUpdateExchangeRate] for non-owner', async function() {
863 | let notOwner = publicKeys[2];
864 | let notOwnerPrivateKey = privateKeys[2];
865 |
866 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
867 | let exchangeRate = csDetails[4].toNumber();
868 |
869 | let data = web3Contract.crowdsaleUpdateExchangeRate.getData(
870 | 'crowdsale1',
871 | 5
872 | );
873 |
874 | let errorMessage;
875 | try {
876 | await rawTransaction(
877 | notOwner,
878 | notOwnerPrivateKey,
879 | contract.address,
880 | data,
881 | 0
882 | );
883 | } catch (error) {
884 | errorMessage = error.message;
885 | }
886 |
887 | let csDetailsAfter = await contract.getCrowdsaleDetails.call('crowdsale1');
888 | let exchangeRateAfter = csDetailsAfter[4].toNumber();
889 |
890 | exchangeRate = exchangeRate.toString();
891 | exchangeRateAfter = exchangeRateAfter.toString();
892 |
893 | assert.strictEqual(exchangeRate, exchangeRateAfter);
894 |
895 | let expected = 'VM Exception while processing transaction: revert';
896 | assert.strictEqual(errorMessage, expected);
897 | });
898 |
899 | it('should fail [crowdsaleUpdateExchangeRate] bad name', async function() {
900 | let ownerPrivateKey = privateKeys[0];
901 |
902 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
903 | let exchangeRate = csDetails[4].toNumber();
904 |
905 | let data = web3Contract.crowdsaleUpdateExchangeRate.getData(
906 | 'badnameforcrowdsale',
907 | 5
908 | );
909 |
910 | let errorMessage;
911 | try {
912 | await rawTransaction(
913 | owner,
914 | ownerPrivateKey,
915 | contract.address,
916 | data,
917 | 0
918 | );
919 | } catch (error) {
920 | errorMessage = error.message;
921 | }
922 |
923 | let csDetailsAfter = await contract.getCrowdsaleDetails.call('crowdsale1');
924 | let exchangeRateAfter = csDetailsAfter[4].toNumber();
925 |
926 | exchangeRate = exchangeRate.toString();
927 | exchangeRateAfter = exchangeRateAfter.toString();
928 |
929 | assert.strictEqual(exchangeRate, exchangeRateAfter);
930 |
931 | let expected = 'VM Exception while processing transaction: revert';
932 | assert.strictEqual(errorMessage, expected);
933 | });
934 |
935 | it('should [crowdsaleUpdateExchangeRate] for owner only', async function() {
936 | let ownerPrivateKey = privateKeys[0];
937 |
938 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
939 | let exchangeRate = csDetails[4].toNumber();
940 |
941 | let data = web3Contract.crowdsaleUpdateExchangeRate.getData(
942 | 'crowdsale1',
943 | 500
944 | );
945 |
946 | let result = await rawTransaction(
947 | owner,
948 | ownerPrivateKey,
949 | contract.address,
950 | data,
951 | 0
952 | );
953 |
954 | let csDetailsAfter = await contract.getCrowdsaleDetails.call('crowdsale1');
955 | let exchangeRateAfter = csDetailsAfter[4].toNumber();
956 |
957 | exchangeRate = exchangeRate.toString();
958 | exchangeRateAfter = exchangeRateAfter.toString();
959 |
960 | assert.strictEqual(exchangeRate, '400');
961 | assert.strictEqual(exchangeRateAfter, '500');
962 | assert.strictEqual(0, result.indexOf('0x'));
963 | });
964 |
965 | it('should fail [crowdsalePurchase] purchase too large', async function() {
966 | let ownerPrivateKey = privateKeys[0];
967 |
968 | let ownerBalance = await contract.balanceOf.call(owner);
969 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
970 | let exchangeRate = csDetails[4].toNumber();
971 | let csTokenBalance = csDetails[3];
972 | let value = 500000;
973 |
974 | let data = web3Contract.crowdsalePurchase.getData('crowdsale1', owner);
975 |
976 | let errorMessage;
977 | try {
978 | await rawTransaction(
979 | owner,
980 | ownerPrivateKey,
981 | contract.address,
982 | data,
983 | value
984 | );
985 | } catch (error) {
986 | errorMessage = error.message;
987 | }
988 |
989 | let ownerBalanceAfter = await contract.balanceOf.call(owner);
990 | let csDetailsAfter = await contract.getCrowdsaleDetails.call('crowdsale1');
991 | let csTokenBalanceAfter = csDetailsAfter[3];
992 |
993 | ownerBalance = ownerBalance.toString();
994 | ownerBalanceAfter = ownerBalanceAfter.toString();
995 | csTokenBalance = csTokenBalance.toString();
996 | csTokenBalanceAfter = csTokenBalanceAfter.toString();
997 |
998 | assert.strictEqual(ownerBalance, ownerBalanceAfter);
999 | assert.strictEqual(csTokenBalance, csTokenBalanceAfter);
1000 |
1001 | let expected = 'VM Exception while processing transaction: revert';
1002 | assert.strictEqual(errorMessage, expected);
1003 | });
1004 |
1005 | it('should fail [crowdsalePurchase] for bad name', async function() {
1006 | let ownerPrivateKey = privateKeys[0];
1007 |
1008 | let ownerBalance = await contract.balanceOf.call(owner);
1009 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
1010 | let exchangeRate = csDetails[4].toNumber();
1011 | let csTokenBalance = csDetails[3];
1012 | let value = 5;
1013 |
1014 | let data = web3Contract.crowdsalePurchase.getData(
1015 | 'notacrowdsalename',
1016 | owner
1017 | );
1018 |
1019 | let errorMessage;
1020 | try {
1021 | await rawTransaction(
1022 | owner,
1023 | ownerPrivateKey,
1024 | contract.address,
1025 | data,
1026 | value
1027 | );
1028 | } catch (error) {
1029 | errorMessage = error.message;
1030 | }
1031 |
1032 | let ownerBalanceAfter = await contract.balanceOf.call(owner);
1033 | let csTokenBalanceAfter =
1034 | await contract.crowdsaleTokenBalance.call('crowdsale1');
1035 |
1036 | ownerBalance = ownerBalance.toString();
1037 | ownerBalanceAfter = ownerBalanceAfter.toString();
1038 | csTokenBalance = csTokenBalance.toString();
1039 | csTokenBalanceAfter = csTokenBalanceAfter.toString();
1040 |
1041 | assert.strictEqual(ownerBalance, ownerBalanceAfter);
1042 | assert.strictEqual(csTokenBalance, csTokenBalanceAfter);
1043 |
1044 | let expected = 'VM Exception while processing transaction: revert';
1045 | assert.strictEqual(errorMessage, expected);
1046 | });
1047 |
1048 | it('should fail [crowdsalePurchase] for empty name', async function() {
1049 | let ownerPrivateKey = privateKeys[0];
1050 |
1051 | let ownerBalance = await contract.balanceOf.call(owner);
1052 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
1053 | let exchangeRate = csDetails[4].toNumber();
1054 | let csTokenBalance = csDetails[3];
1055 | let value = 5;
1056 |
1057 | let data = web3Contract.crowdsalePurchase.getData('', owner);
1058 |
1059 | let errorMessage;
1060 | try {
1061 | await rawTransaction(
1062 | owner,
1063 | ownerPrivateKey,
1064 | contract.address,
1065 | data,
1066 | value
1067 | );
1068 | } catch (error) {
1069 | errorMessage = error.message;
1070 | }
1071 |
1072 | let ownerBalanceAfter = await contract.balanceOf.call(owner);
1073 | let csTokenBalanceAfter =
1074 | await contract.crowdsaleTokenBalance.call('crowdsale1');
1075 |
1076 | ownerBalance = ownerBalance.toString();
1077 | ownerBalanceAfter = ownerBalanceAfter.toString();
1078 | csTokenBalance = csTokenBalance.toString();
1079 | csTokenBalanceAfter = csTokenBalanceAfter.toString();
1080 |
1081 | assert.strictEqual(ownerBalance, ownerBalanceAfter);
1082 | assert.strictEqual(csTokenBalance, csTokenBalanceAfter);
1083 |
1084 | let expected = 'VM Exception while processing transaction: revert';
1085 | assert.strictEqual(errorMessage, expected);
1086 | });
1087 |
1088 | it('should properly [crowdsalePurchase] for owner', async function() {
1089 | let ownerPrivateKey = privateKeys[0];
1090 |
1091 | let ownerBalance = await contract.balanceOf.call(owner);
1092 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
1093 | let exchangeRate = csDetails[4].toNumber();
1094 | let csTokenBalance = csDetails[3];
1095 | let value = 5;
1096 |
1097 | let data = web3Contract.crowdsalePurchase.getData('crowdsale1', owner);
1098 |
1099 | let result = await rawTransaction(
1100 | owner,
1101 | ownerPrivateKey,
1102 | contract.address,
1103 | data,
1104 | value
1105 | );
1106 |
1107 | let ownerBalanceAfter = await contract.balanceOf.call(owner);
1108 | let csTokenBalanceAfter =
1109 | await contract.crowdsaleTokenBalance.call('crowdsale1');
1110 |
1111 | ownerBalance = ownerBalance.toString();
1112 | ownerBalanceAfter = ownerBalanceAfter.toString();
1113 | csTokenBalance = csTokenBalance.toString();
1114 | csTokenBalanceAfter = csTokenBalanceAfter.toString();
1115 |
1116 | assert.strictEqual(ownerBalance, '9.999999999999999999939e+26');
1117 | assert.strictEqual(ownerBalanceAfter, '9.999999999999999999939025e+26');
1118 | assert.strictEqual(csTokenBalance, '50000');
1119 | assert.strictEqual(csTokenBalanceAfter, '47500');
1120 | assert.strictEqual(0, result.indexOf('0x'));
1121 | });
1122 |
1123 | it('should properly [crowdsalePurchase] for non-owner', async function() {
1124 | let beneficiary = publicKeys[3];
1125 | let notOwner = publicKeys[1];
1126 | let notOwnerPrivateKey = privateKeys[1];
1127 |
1128 | let beneficiaryBalance = await contract.balanceOf.call(beneficiary);
1129 | let notOwnerBalance = await contract.balanceOf.call(notOwner);
1130 | let csDetails = await contract.getCrowdsaleDetails.call('crowdsale1');
1131 | let exchangeRate = csDetails[4].toNumber();
1132 | let csTokenBalance = csDetails[3];
1133 | let value = 5;
1134 |
1135 | let data = web3Contract.crowdsalePurchase.getData(
1136 | 'crowdsale1',
1137 | beneficiary
1138 | );
1139 |
1140 | let result = await rawTransaction(
1141 | notOwner,
1142 | notOwnerPrivateKey,
1143 | contract.address,
1144 | data,
1145 | value
1146 | );
1147 |
1148 | let beneficiaryBalanceAfter = await contract.balanceOf.call(beneficiary);
1149 | let notOwnerBalanceAfter = await contract.balanceOf.call(notOwner);
1150 | let csTokenBalanceAfter =
1151 | await contract.crowdsaleTokenBalance.call('crowdsale1');
1152 |
1153 | notOwnerBalance = notOwnerBalance.toString();
1154 | notOwnerBalanceAfter = notOwnerBalanceAfter.toString();
1155 | beneficiaryBalance = beneficiaryBalance.toString();
1156 | beneficiaryBalanceAfter = beneficiaryBalanceAfter.toString();
1157 | csTokenBalance = csTokenBalance.toString();
1158 | csTokenBalanceAfter = csTokenBalanceAfter.toString();
1159 |
1160 | assert.strictEqual(notOwnerBalance, notOwnerBalanceAfter);
1161 | assert.strictEqual(beneficiaryBalance, '0');
1162 | assert.strictEqual(beneficiaryBalanceAfter, '2500');
1163 | assert.strictEqual(csTokenBalance, '47500');
1164 | assert.strictEqual(csTokenBalanceAfter, '45000');
1165 | assert.strictEqual(0, result.indexOf('0x'));
1166 | });
1167 |
1168 | it('should account for every [event] execution', function(done) {
1169 | wait(5000).then(() => {
1170 | assert.strictEqual(eventCounter.Transfer, 7);
1171 | assert.strictEqual(eventCounter.Approval, 1);
1172 | assert.strictEqual(eventCounter.TokenNameChanged, 1);
1173 | assert.strictEqual(eventCounter.TokenSymbolChanged, 1);
1174 | assert.strictEqual(eventCounter.CrowdsaleDeployed, 2);
1175 | done();
1176 | });
1177 | });
1178 | });
1179 |
1180 | /*
1181 | * Call a smart contract function from any keyset in which the caller has the
1182 | * private and public keys.
1183 | * @param {string} senderPublicKey Public key in key pair.
1184 | * @param {string} senderPrivateKey Private key in key pair.
1185 | * @param {string} contractAddress Address of Solidity contract.
1186 | * @param {string} data Data from the function's `getData` in web3.js.
1187 | * @param {number} value Number of Ethereum wei sent in the transaction.
1188 | * @return {Promise}
1189 | */
1190 | function rawTransaction(
1191 | senderPublicKey,
1192 | senderPrivateKey,
1193 | contractAddress,
1194 | data,
1195 | value
1196 | ) {
1197 | return new Promise((resolve, reject) => {
1198 |
1199 | let key = new Buffer(senderPrivateKey, 'hex');
1200 | let nonce = web3.toHex(web3.eth.getTransactionCount(senderPublicKey));
1201 |
1202 | let gasPrice = web3.eth.gasPrice;
1203 | let gasPriceHex = web3.toHex(web3.eth.estimateGas({
1204 | from: contractAddress
1205 | }));
1206 | let gasLimitHex = web3.toHex(5500000);
1207 |
1208 | let rawTx = {
1209 | nonce: nonce,
1210 | gasPrice: gasPriceHex,
1211 | gasLimit: gasLimitHex,
1212 | data: data,
1213 | to: contractAddress,
1214 | value: web3.toHex(value)
1215 | };
1216 |
1217 | let tx = new EthereumTx(rawTx);
1218 | tx.sign(key);
1219 |
1220 | let stx = '0x' + tx.serialize().toString('hex');
1221 |
1222 | web3.eth.sendRawTransaction(stx, (err, hash) => {
1223 | if (err) {
1224 | reject(err);
1225 | } else {
1226 | resolve(hash);
1227 | }
1228 | });
1229 |
1230 | });
1231 | }
1232 |
1233 | function wait (ms) {
1234 | return new Promise((resolve, reject) => {
1235 | setTimeout(resolve, ms);
1236 | });
1237 | };
1238 |
--------------------------------------------------------------------------------