├── package.json ├── README.md ├── examples └── pi-day-n00b-token.js ├── AirDropToken.sol ├── test-contract.js ├── bin └── ethers-airdrop.js └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethers-airdrop", 3 | "version": "0.0.1", 4 | "description": "A simple example of how to use Merkle trees to Air Drop tokens.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ethers": "^3.0.0", 8 | "ethers-cli": "^3.0.0" 9 | }, 10 | "devDependencies": { 11 | "mocha": "^3.2.0" 12 | }, 13 | "scripts": { 14 | "test": "./node_modules/.bin/mocha test-contract.js" 15 | }, 16 | "keywords": [ 17 | "Ethereum", 18 | "Air", 19 | "Drop", 20 | "merkle", 21 | "trees" 22 | ], 23 | "author": "Richard Moore ", 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Merkle Air-Drop 2 | =============== 3 | 4 | Quickly, efficiently and affordably disperse a Token Air-Drop to many account 5 | using Merkle trees. 6 | 7 | Features 8 | -------- 9 | 10 | - Fixed-cost and low-cost deployment of **any** number of tokens 11 | - All tokens are immediately available to be claimed 12 | - Anyone can pay the gas 13 | - Addresses which cannot spend the tokens do not waste storage or seed transactions 14 | - Tokens can be kept off-chain until needed 15 | 16 | 17 | Command Line Interface 18 | ---------------------- 19 | 20 | ``` 21 | Command Line Interface - ethers-airdrop/0.0.1 22 | 23 | Usage: 24 | ethers-airdrop deploy TITLE SYMBOL [ DECIMALS ] --account ACCOUNT 25 | ethers-airdrop redeem CONTRACT_ADDRESS INDEX --account ACCOUNT 26 | ethers-airdrop lookup CONTRACT_ADDRESS INDEX_OR_ADDRESS 27 | 28 | Options: 29 | --balances AirDrop Balance Data (default: ./airdrop-balances.json) 30 | ``` 31 | 32 | 33 | License 34 | ------- 35 | 36 | MIT License 37 | 38 | 39 | Donations 40 | --------- 41 | 42 | Ethereum: `0x59DEa134510ebce4a0c7146595dc8A61Eb9D0D79` 43 | -------------------------------------------------------------------------------- /examples/pi-day-n00b-token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | var ethers = require('ethers'); 6 | 7 | var convert = require('ethers/utils/convert'); 8 | function getBlockNumber(blockNumber) { 9 | return convert.hexStripZeros(convert.hexlify(blockNumber)); 10 | } 11 | 12 | function now() { 13 | return (new Date()).getTime() / 1000; 14 | } 15 | 16 | var total = ethers.utils.bigNumberify(0); 17 | var provider = new ethers.providers.JsonRpcProvider(); 18 | var balances = {}; 19 | 20 | function getBalances(startBlockNumber, count) { 21 | var t0 = now(); 22 | function check(blockNumber) { 23 | console.log(blockNumber); 24 | provider.send('eth_getBlockByNumber', [getBlockNumber(blockNumber), true]).then(function(block) { 25 | block.transactions.forEach(function(tx) { 26 | if (tx.nonce !== '0x0') { return; } 27 | if (!balances[tx.from]) { 28 | balances[tx.from] = ethers.utils.bigNumberify(tx.value); 29 | } else { 30 | console.log('This should not happen...'); 31 | balances[tx.from].add(tx.value); 32 | } 33 | total = total.add(tx.value); 34 | }); 35 | 36 | if (Object.keys(balances).length > count) { 37 | Object.keys(balances).forEach(function(a) { 38 | balances[a] = balances[a].toHexString(); 39 | }); 40 | console.log({ 41 | blocks: startBlockNumber - blockNumber, 42 | dt: now() - t0, 43 | accounts: Object.keys(balances).length, 44 | total: ethers.utils.formatUnits(total, 18) 45 | }); 46 | fs.writeFileSync('airdrop-balances.json', JSON.stringify(balances)); 47 | } else { 48 | setTimeout(function() { check(blockNumber - 1, count); }, 0); 49 | } 50 | }); 51 | } 52 | check(startBlockNumber); 53 | } 54 | 55 | var firstPiDayBlock = 5251718; 56 | getBalances(firstPiDayBlock, 10000); 57 | /* 58 | provider.getBlockNumber().then(function(blockNumber) { 59 | getBalances(blockNumber, 10000); 60 | }); 61 | */ 62 | -------------------------------------------------------------------------------- /AirDropToken.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * Merkle Air-Drop Token 3 | * 4 | * See: https://blog.ricmoo.com/merkle-air-drops-e6406945584d 5 | */ 6 | 7 | pragma solidity ^0.4.17; 8 | 9 | contract AirDropToken { 10 | 11 | event Transfer(address indexed from, address indexed to, uint256 tokens); 12 | event Approval(address indexed tokenOwner, address indexed spender, uint256 tokens); 13 | 14 | string _name; 15 | string _symbol; 16 | uint8 _decimals; 17 | 18 | uint256 _totalSupply; 19 | 20 | bytes32 _rootHash; 21 | 22 | mapping (address => uint256) _balances; 23 | mapping (address => mapping(address => uint256)) _allowed; 24 | 25 | mapping (uint256 => uint256) _redeemed; 26 | 27 | function AirDropToken(string name, string symbol, uint8 decimals, bytes32 rootHash, uint256 premine) public { 28 | _name = name; 29 | _symbol = symbol; 30 | _decimals = decimals; 31 | _rootHash = rootHash; 32 | 33 | if (premine > 0) { 34 | _balances[msg.sender] = premine; 35 | _totalSupply = premine; 36 | Transfer(0, msg.sender, premine); 37 | } 38 | } 39 | 40 | function name() public constant returns (string name) { 41 | return _name; 42 | } 43 | 44 | function symbol() public constant returns (string symbol) { 45 | return _symbol; 46 | } 47 | 48 | function decimals() public constant returns (uint8 decimals) { 49 | return _decimals; 50 | } 51 | 52 | function totalSupply() public constant returns (uint256 totalSupply) { 53 | return _totalSupply; 54 | } 55 | 56 | function balanceOf(address tokenOwner) public constant returns (uint256 balance) { 57 | return _balances[tokenOwner]; 58 | } 59 | 60 | function allowance(address tokenOwner, address spender) public constant returns (uint256 remaining) { 61 | return _allowed[tokenOwner][spender]; 62 | } 63 | 64 | function transfer(address to, uint256 amount) public returns (bool success) { 65 | if (_balances[msg.sender] < amount) { return false; } 66 | 67 | _balances[msg.sender] -= amount; 68 | _balances[to] += amount; 69 | 70 | Transfer(msg.sender, to, amount); 71 | 72 | return true; 73 | } 74 | 75 | function transferFrom(address from, address to, uint256 amount) public returns (bool success) { 76 | 77 | if (_allowed[from][msg.sender] < amount || _balances[from] < amount) { 78 | return false; 79 | } 80 | 81 | _balances[from] -= amount; 82 | _allowed[from][msg.sender] -= amount; 83 | _balances[to] += amount; 84 | 85 | Transfer(from, to, amount); 86 | 87 | return true; 88 | } 89 | 90 | function approve(address spender, uint256 amount) public returns (bool success) { 91 | _allowed[msg.sender][spender] = amount; 92 | 93 | Approval(msg.sender, spender, amount); 94 | 95 | return true; 96 | } 97 | 98 | function redeemed(uint256 index) public constant returns (bool redeemed) { 99 | uint256 redeemedBlock = _redeemed[index / 256]; 100 | uint256 redeemedMask = (uint256(1) << uint256(index % 256)); 101 | return ((redeemedBlock & redeemedMask) != 0); 102 | } 103 | 104 | function redeemPackage(uint256 index, address recipient, uint256 amount, bytes32[] merkleProof) public { 105 | 106 | // Make sure this package has not already been claimed (and claim it) 107 | uint256 redeemedBlock = _redeemed[index / 256]; 108 | uint256 redeemedMask = (uint256(1) << uint256(index % 256)); 109 | require((redeemedBlock & redeemedMask) == 0); 110 | _redeemed[index / 256] = redeemedBlock | redeemedMask; 111 | 112 | // Compute the merkle root 113 | bytes32 node = keccak256(index, recipient, amount); 114 | uint256 path = index; 115 | for (uint16 i = 0; i < merkleProof.length; i++) { 116 | if ((path & 0x01) == 1) { 117 | node = keccak256(merkleProof[i], node); 118 | } else { 119 | node = keccak256(node, merkleProof[i]); 120 | } 121 | path /= 2; 122 | } 123 | 124 | // Check the merkle proof 125 | require(node == _rootHash); 126 | 127 | // Redeem! 128 | _balances[recipient] += amount; 129 | _totalSupply += amount; 130 | 131 | Transfer(0, recipient, amount); 132 | } 133 | } 134 | 135 | /* 136 | contract AirDropFactory { 137 | event Create(address tokenContract); 138 | 139 | function createAirDrop(string name, string symbol, uint8 decimals, bytes32 rootHash, uint256 premine) { 140 | AirDropToken airDropToken = new AirDropToken(name, symbol, decimals, rootHash, premine); 141 | Create(address(airDropToken)); 142 | } 143 | } 144 | */ 145 | -------------------------------------------------------------------------------- /test-contract.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | var ethers = require('ethers'); 6 | var tools = require('ethers-cli'); 7 | 8 | // Obviously a lot more is needed here; like transfer, et cetera. 9 | 10 | var AirDrop = require('./index'); 11 | 12 | function balanceOfIndex(index) { 13 | return ethers.utils.parseUnits(String(1 + 10 * index), 18); 14 | } 15 | 16 | function deployAirDrop(count) { 17 | 18 | // Construct a new test builder 19 | var builder = new tools.TestBuilder(function(builder) { 20 | var balances = {}; 21 | for (var i = 0; i < count; i++) { 22 | balances[builder.accounts[i].address] = balanceOfIndex(i); 23 | } 24 | 25 | var airDrop = new AirDrop(balances); 26 | 27 | var codes = builder.compile('./AirDropToken.sol', true); 28 | 29 | var airDropTokenCode = codes.AirDropToken; 30 | 31 | return airDropTokenCode.deploy('Air Drop Token', 'ADT', 18, airDrop.rootHash, 10).then(function(contract) { 32 | return { 33 | airDrop: airDrop, 34 | builder: builder, 35 | contract: contract 36 | }; 37 | }); 38 | }); 39 | 40 | // Deploy the contract into this builder 41 | return builder.deploy(); 42 | } 43 | 44 | 45 | describe('Basic Tests', function() { 46 | it('correctly deploys', function() { 47 | this.timeout(120000); 48 | 49 | return deployAirDrop(8).then(function(deployed) { 50 | var builder = deployed.builder; 51 | 52 | var tokenAdmin = deployed.contract; 53 | 54 | var tokenReadOnly = tokenAdmin.connect(builder.provider); 55 | 56 | return Promise.all([ 57 | tokenReadOnly.balanceOf(builder.accounts[0].address), 58 | tokenReadOnly.balanceOf(builder.accounts[1].address), 59 | tokenReadOnly.name(), 60 | tokenReadOnly.symbol(), 61 | tokenReadOnly.decimals(), 62 | tokenReadOnly.totalSupply(), 63 | ]).then(function(result) { 64 | assert.equal(result[0].toNumber(), 10, 'premined balance assigned') 65 | assert.equal(result[1].toNumber(), 0, 'other balance is empty') 66 | assert.equal(result[2], 'Air Drop Token', 'name is correct') 67 | assert.equal(result[3], 'ADT', 'symbol is correct') 68 | assert.equal(result[4], 18, 'decimals is correct') 69 | assert.equal(result[5].toNumber(), 10, 'total suppl is correct') 70 | }); 71 | }); 72 | }); 73 | 74 | function testRedeem(index, count) { 75 | return deployAirDrop(8).then(function(deployed) { 76 | var builder = deployed.builder; 77 | 78 | var airDrop = builder.deployed.airDrop; 79 | 80 | var user = builder.accounts[1]; 81 | var index = airDrop.getIndex(user.address);; 82 | var amount = balanceOfIndex(1); 83 | 84 | var tokenAdmin = builder.deployed.contract; 85 | var tokenReadOnly = tokenAdmin.connect(builder.provider); 86 | var tokenOtherUser = tokenAdmin.connect(builder.accounts[2]); 87 | 88 | var proof = airDrop.getMerkleProof(index); 89 | 90 | var seq = Promise.resolve(); 91 | 92 | seq = seq.then(function() { 93 | return tokenReadOnly.balanceOf(user.address).then(function(balance) { 94 | assert.equal(balance.toNumber(), 0, 'initial balance is zero'); 95 | }); 96 | }); 97 | 98 | seq = seq.then(function() { 99 | return tokenOtherUser.redeemPackage(index, user.address, amount, proof).then(function(tx) { 100 | console.log(tx); 101 | }); 102 | }); 103 | 104 | seq = seq.then(function() { 105 | return tokenReadOnly.balanceOf(user.address).then(function(balance) { 106 | assert.ok(balance.eq(amount), 'final balance is correct'); 107 | }); 108 | }); 109 | /* 110 | seq = seq.then(function() { 111 | return tokenOtherUser.redeemPackage(index, user.address, amount, proof).then(function(tx) { 112 | assert.ok(false, 'duplicate redeem did not fail'); 113 | }, function(error) { 114 | assert.ok(true, 'duplicate redeem failed'); 115 | }); 116 | }); 117 | */ 118 | return seq; 119 | }); 120 | } 121 | 122 | [1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17].forEach(function(count) { 123 | for (var i = 0; i < count; i++) { 124 | (function(index) { 125 | it('allows a user index ' + index + ' to redeem a token pacakge from count ' + count, function() { 126 | this.timeout(120000); 127 | return testRedeem(index, count); 128 | }); 129 | })(i); 130 | } 131 | }); 132 | 133 | }); 134 | -------------------------------------------------------------------------------- /bin/ethers-airdrop.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | 7 | var ethers = require('ethers'); 8 | var getopts = require('ethers-cli/lib/getopts'); 9 | 10 | var AirDrop = require('..'); 11 | var version = require('../package.json').version; 12 | 13 | var options = { 14 | balances: 'airdrop-balances.json', 15 | premine: '0', 16 | 17 | help: false, 18 | version: false, 19 | 20 | _accounts: true, 21 | _provider: true, 22 | _promises: true, 23 | }; 24 | 25 | getopts(options).then(function(opts) { 26 | if (opts.options.help) { getopts.throwError(); } 27 | 28 | if (opts.options.version) { 29 | console.log('ethers-airdrop/' + version); 30 | return function() { } 31 | } 32 | 33 | var airDrop = new AirDrop(JSON.parse(fs.readFileSync(opts.options.balances)), opts.options.account); 34 | console.log('Loaded Air Drop Balances.'); 35 | 36 | if (opts.args.length === 0) { getopts.throwError('no command provided'); } 37 | 38 | var command = opts.args.shift(); 39 | 40 | switch (command) { 41 | 42 | case 'deploy': return (function() { 43 | if (opts.args.length === 3) { 44 | var decimals = parseInt(opts.args.pop()); 45 | } 46 | if (opts.args.length !== 2) { 47 | getopts.throwError('deploy requires TITLE, SYMBOL'); 48 | } 49 | var title = opts.args.shift(); 50 | var symbol = opts.args.shift(); 51 | var premine = ethers.utils.parseUnits(opts.options.premine, 18); 52 | if (opts.accounts.length !== 1) { 53 | getopts.throwError('deploy requires --account'); 54 | } 55 | return (function() { 56 | return airDrop.deploy(opts.accounts[0], title, symbol, decimals, premine).then(function(tx) { 57 | console.log(tx); 58 | console.log('Deployed:'); 59 | console.log(' Transaction: ' + tx.hash); 60 | console.log(' Contract Address: ' + tx.contractAddress); 61 | console.log(''); 62 | }); 63 | }); 64 | })(); 65 | 66 | case 'redeem': return (function() { 67 | if (opts.args.length !== 2) { throw new Error('redeem requires CONTRACT_ADDRESS and INDEX_OR_ADDRESS'); } 68 | var contractAddress = opts.args.shift(); 69 | var index = opts.args.shift(); 70 | if (!index.match(/^[0-9]+$/)) { 71 | index = airDrop.getIndex(ethers.utils.getAddress(index)); 72 | } else { 73 | var index = parseInt(index); 74 | } 75 | return (function() { 76 | return airDrop.redeem(opts.accounts[0], contractAddress, index).then(function(tx) { 77 | console.log('Redeem: (Contract: ' + contractAddress + ')'); 78 | console.log(' Transaction: ' + tx.hash); 79 | console.log(''); 80 | }); 81 | }); 82 | })(); 83 | 84 | case 'lookup': return (function() { 85 | if (opts.args.length !== 2) { throw new Error('lookup requires CONTRACT_ADDRESS and INDEX_OR_ADDRESS'); } 86 | var contractAddress = opts.args.shift(); 87 | var index = opts.args.shift(); 88 | if (!index.match(/^[0-9]+$/)) { 89 | index = airDrop.getIndex(ethers.utils.getAddress(index)); 90 | } 91 | return (function() { 92 | var address = airDrop.getAddress(index); 93 | return Promise.all([ 94 | airDrop.getRedeemed(opts.provider, contractAddress, index), 95 | airDrop.getInfo(opts.provider, contractAddress), 96 | airDrop.getBalance(opts.provider, contractAddress, address) 97 | ]).then(function(result) { 98 | console.log('Lookup: (Contract: ' + contractAddress + ')'); 99 | console.log(' Address: ' + address); 100 | console.log(' Index: ' + index); 101 | console.log(' Name: ' + result[1].name); 102 | console.log(' Symbol: ' + result[1].symbol); 103 | console.log(' Decimals: ' + result[1].decimals); 104 | console.log(' Total Supply: ' + ethers.utils.formatUnits(result[1].totalSupply, result[1].decimals)); 105 | console.log(' Amount: ' + ethers.utils.formatUnits(airDrop.getAmount(index), result[1].decimals)); 106 | console.log(' Balance: ' + ethers.utils.formatUnits(result[2], result[1].decimals)); 107 | console.log(' Redeemed: ' + (result[0] ? 'yes': 'no')); 108 | console.log(''); 109 | }); 110 | }); 111 | })(); 112 | 113 | default: 114 | getopts.throwError('unknown command: ' + command); 115 | } 116 | 117 | }).then(function(run) { 118 | return run(); 119 | 120 | }, function(error) { 121 | console.log(''); 122 | console.log('Command Line Interface - ethers-airdrop/' + version); 123 | console.log(''); 124 | console.log('Usage:'); 125 | console.log(' ethers-airdrop deploy TITLE SYMBOL [ DECIMALS ] --account ACCOUNT'); 126 | console.log(' ethers-airdrop redeem CONTRACT_ADDRESS INDEX --account ACCOUNT'); 127 | console.log(' ethers-airdrop lookup CONTRACT_ADDRESS INDEX_OR_ADDRESS'); 128 | console.log(''); 129 | console.log('Options:'); 130 | console.log(' --balances AirDrop Balance Data (default: ./airdrop-balances.json)'); 131 | console.log(''); 132 | 133 | if (error.message) { throw error; } 134 | console.log(''); 135 | 136 | }).catch(function(error) { 137 | console.log(''); 138 | if (!error._messageOnly) { 139 | console.log(error.stack); 140 | } else { 141 | console.log('Error: ' + error.message); 142 | } 143 | console.log(''); 144 | }); 145 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | var solc = null; 7 | 8 | var ethers = require('ethers'); 9 | 10 | function compile(sourceCode) { 11 | if (!solc) { solc = require('solc'); } 12 | var contracts = solc.compile(sourceCode, 1) 13 | return contracts; 14 | } 15 | 16 | var abiFragment = [ 17 | { 18 | inputs: [ 19 | { name: 'name', type: 'string' }, 20 | { name: 'symbol', type: 'string' }, 21 | { name: 'decimals', type: 'uint8' }, 22 | { name: 'rootHash', type: 'bytes32' }, 23 | { name: 'premine', type: 'uint256' }, 24 | ], 25 | outputs: [], 26 | payable: false, 27 | type: 'constructor' 28 | }, 29 | { 30 | name: 'redeemed', 31 | inputs: [ 32 | { name: 'index', type: 'uint256' } 33 | ], 34 | outputs: [ 35 | { name: 'redeemed', type: 'bool' } 36 | ], 37 | constant: true, 38 | type: 'function' 39 | }, 40 | { 41 | name: 'name', 42 | inputs: [], 43 | outputs: [ 44 | { name: 'name', type: 'string' } 45 | ], 46 | constant: true, 47 | type: 'function' 48 | }, 49 | { 50 | name: 'symbol', 51 | inputs: [], 52 | outputs: [ 53 | { name: 'symbol', type: 'string' } 54 | ], 55 | constant: true, 56 | type: 'function' 57 | }, 58 | { 59 | name: 'decimals', 60 | inputs: [], 61 | outputs: [ 62 | { name: 'decimals', type: 'uint8' } 63 | ], 64 | constant: true, 65 | type: 'function' 66 | }, 67 | { 68 | name: 'totalSupply', 69 | inputs: [], 70 | outputs: [ 71 | { name: 'totalSupply', type: 'uint256' } 72 | ], 73 | constant: true, 74 | type: 'function' 75 | }, 76 | { 77 | name: 'balanceOf', 78 | inputs: [ 79 | { name: 'owner', type: 'address' } 80 | ], 81 | outputs: [ 82 | { name: 'balance', type: 'uint256' } 83 | ], 84 | constant: true, 85 | type: 'function' 86 | }, 87 | { 88 | name: 'redeemPackage', 89 | inputs: [ 90 | { name: 'index', type: 'uint256' }, 91 | { name: 'recipient', type: 'address' }, 92 | { name: 'amount', type: 'uint256' }, 93 | { name: 'proof', type: 'bytes32[]' } 94 | ], 95 | outputs: [], 96 | payable: false, 97 | constant: false, 98 | type: 'function' 99 | } 100 | ]; 101 | 102 | function reduceMerkleBranches(leaves) { 103 | var output = []; 104 | 105 | while (leaves.length) { 106 | var left = leaves.shift(); 107 | var right = (leaves.length === 0) ? left: leaves.shift(); 108 | //output.push(ethers.utils.keccak256(ethers.utils.concat([ left, right ]))); 109 | output.push(ethers.utils.keccak256(left + right.substring(2))); 110 | } 111 | 112 | output.forEach(function(leaf) { 113 | leaves.push(leaf); 114 | }); 115 | } 116 | 117 | var t0 = (new Date()).getTime() 118 | function now() { 119 | return (new Date()).getTime() - t0; 120 | } 121 | 122 | function expandLeaves(balances) { 123 | var addresses = Object.keys(balances); 124 | 125 | addresses.sort(function(a, b) { 126 | var al = a.toLowerCase(), bl = b.toLowerCase(); 127 | if (al < bl) { return -1; } 128 | if (al > bl) { return 1; } 129 | return 0; 130 | }); 131 | 132 | return addresses.map(function(a, i) { return { address: a, balance: balances[a], index: i }; }); 133 | } 134 | 135 | // ethers.utils.solidityKeccak256(types, [ leaf.index, leaf.address, leaf.balance ]); 136 | var zeros32 = '0000000000000000000000000000000000000000000000000000000000000000'; 137 | function hash(index, address, balance) { 138 | index = zeros32 + (index).toString(16); 139 | index = index.substring(index.length - 64); 140 | address = address.substring(2) 141 | balance = zeros32 + balance.substring(2); 142 | balance = balance.substring(balance.length - 64); 143 | return ethers.utils.keccak256('0x' + index + address + balance); 144 | } 145 | 146 | function getLeaves(balances) { 147 | var leaves = expandLeaves(balances); 148 | 149 | return leaves.map(function(leaf) { 150 | return hash(leaf.index, leaf.address, leaf.balance); 151 | }); 152 | } 153 | 154 | function computeRootHash(balances) { 155 | var leaves = getLeaves(balances); 156 | 157 | while (leaves.length > 1) { 158 | reduceMerkleBranches(leaves); 159 | } 160 | 161 | return leaves[0]; 162 | } 163 | 164 | function computeMerkleProof(balances, index) { 165 | var leaves = getLeaves(balances); 166 | 167 | if (index == null) { throw new Error('address not found'); } 168 | 169 | var path = index; 170 | 171 | var proof = [ ]; 172 | while (leaves.length > 1) { 173 | if ((path % 2) == 1) { 174 | proof.push(leaves[path - 1]) 175 | } else { 176 | proof.push(leaves[path + 1]) 177 | } 178 | 179 | // Reduce the merkle tree one level 180 | reduceMerkleBranches(leaves); 181 | 182 | // Move up 183 | path = parseInt(path / 2); 184 | } 185 | 186 | return proof; 187 | } 188 | 189 | function AirDrop(balances) { 190 | if (!(this instanceof AirDrop)) { throw new Error('missing new') ;} 191 | 192 | this.balances = balances; 193 | 194 | var rootHash = null; 195 | Object.defineProperty(this, 'rootHash', { 196 | get: function() { 197 | if (rootHash == null) { 198 | rootHash = computeRootHash(balances); 199 | } 200 | return rootHash; 201 | } 202 | }); 203 | 204 | } 205 | 206 | AirDrop.prototype.getIndex = function(address) { 207 | address = address.toLowerCase(); 208 | 209 | var leaves = expandLeaves(this.balances); 210 | 211 | var index = null; 212 | for (var i = 0; i < leaves.length; i++) { 213 | if (i != leaves[i].index) { throw new Error('huh?'); } 214 | if (leaves[i].address === address) { return leaves[i].index; } 215 | } 216 | 217 | throw new Error('address not found'); 218 | } 219 | 220 | AirDrop.prototype.getAddress = function(index) { 221 | var leaves = expandLeaves(this.balances); 222 | return leaves[index].address; 223 | } 224 | 225 | AirDrop.prototype.getAmount = function(index) { 226 | var leaves = expandLeaves(this.balances); 227 | return leaves[index].balance; 228 | } 229 | 230 | AirDrop.prototype.getMerkleProof = function(index) { 231 | return computeMerkleProof(this.balances, index); 232 | } 233 | 234 | AirDrop.prototype.deploy = function(signer, name, symbol, decimals, premine) { 235 | if (arguments.length < 3) { 236 | throw new Error('deploy: signer, name and symbol are required'); 237 | } 238 | if (decimals == null) { decimals = 18; } 239 | if (premine == null) { premine = '0x0'; } 240 | 241 | var sourceCode = fs.readFileSync(path.resolve(__dirname, 'AirDropToken.sol')).toString(); 242 | var bytecode = '0x' + compile(sourceCode).contracts[':AirDropToken'].bytecode; 243 | 244 | var tx = ethers.Contract.getDeployTransaction( 245 | bytecode, 246 | abiFragment, 247 | name, 248 | symbol, 249 | decimals, 250 | this.rootHash, 251 | premine 252 | ); 253 | console.log(tx); 254 | 255 | return signer.sendTransaction(tx).then(function(tx) { 256 | tx.contractAddress = ethers.utils.getContractAddress(tx); 257 | return tx; 258 | }); 259 | } 260 | 261 | AirDrop.prototype.redeem = function(signer, contractAddress, index) { 262 | 263 | var self = this; 264 | 265 | var proof = this.getMerkleProof(index); 266 | console.log('Proof', proof); 267 | 268 | var contract = new ethers.Contract(contractAddress, abiFragment, signer); 269 | return contract.redeemPackage(index, this.getAddress(index), this.getAmount(index), proof).then(function(tx) { 270 | return signer.provider.waitForTransaction(tx.hash).then(function(tx) { 271 | return signer.provider.getTransactionReceipt(tx.hash); 272 | }); 273 | }).then(function(receipt) { 274 | console.log(receipt); 275 | return receipt; 276 | }); 277 | } 278 | 279 | AirDrop.prototype.getBalance = function(provider, contractAddress, address) { 280 | var contract = new ethers.Contract(contractAddress, abiFragment, provider); 281 | return contract.balanceOf(address); 282 | } 283 | 284 | AirDrop.prototype.getRedeemed = function(provider, contractAddress, index) { 285 | var contract = new ethers.Contract(contractAddress, abiFragment, provider); 286 | return contract.redeemed(index); 287 | } 288 | 289 | AirDrop.prototype.getInfo = function(provider, contractAddress) { 290 | var contract = new ethers.Contract(contractAddress, abiFragment, provider); 291 | return Promise.all([ 292 | contract.name(), 293 | contract.symbol(), 294 | contract.decimals(), 295 | contract.totalSupply(), 296 | ]).then(function(result) { 297 | return { 298 | name: result[0], 299 | symbol: result[1], 300 | decimals: result[2], 301 | totalSupply: result[3], 302 | } 303 | }); 304 | } 305 | module.exports = AirDrop; 306 | 307 | --------------------------------------------------------------------------------