From 16cca302b79b588108fab984a5d2e4aba00665a7 Mon Sep 17 00:00:00 2001 From: "Luk.Lu" Date: Tue, 9 Oct 2018 23:03:05 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=94=BE=E5=88=B0=20git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Account.js | 60 ++ README.md | 12 + btc.js | 151 +++++ eth.js | 461 +++++++++++++++ index.js | 16 + netConfig.js | 14 + package.json | 24 + tic.js | 136 +++++ util.js | 35 ++ utils/abi-coder.js | 1018 +++++++++++++++++++++++++++++++++ utils/address.js | 124 ++++ utils/base64.js | 13 + utils/bignumber.js | 149 +++++ utils/browser-base64.js | 24 + utils/browser-random-bytes.js | 43 ++ utils/contract-address.js | 20 + utils/convert.js | 224 ++++++++ utils/empty.js | 1 + utils/errors.js | 91 +++ utils/hdnode.js | 259 +++++++++ utils/hmac.js | 24 + utils/id.js | 10 + utils/index.js | 75 +++ utils/keccak256.js | 12 + utils/namehash.js | 38 ++ utils/pbkdf2.js | 51 ++ utils/properties.js | 22 + utils/random-bytes.js | 8 + utils/rlp.js | 142 +++++ utils/secret-storage.js | 449 +++++++++++++++ utils/sha2.js | 23 + utils/signing-key.js | 104 ++++ utils/solidity.js | 97 ++++ utils/throw-error.js | 11 + utils/units.js | 148 +++++ utils/utf8.js | 113 ++++ utils/words.json | 1 + 37 files changed, 4203 insertions(+) create mode 100644 Account.js create mode 100644 README.md create mode 100644 btc.js create mode 100644 eth.js create mode 100644 index.js create mode 100644 netConfig.js create mode 100644 package.json create mode 100644 tic.js create mode 100644 util.js create mode 100644 utils/abi-coder.js create mode 100644 utils/address.js create mode 100644 utils/base64.js create mode 100644 utils/bignumber.js create mode 100644 utils/browser-base64.js create mode 100644 utils/browser-random-bytes.js create mode 100644 utils/contract-address.js create mode 100644 utils/convert.js create mode 100644 utils/empty.js create mode 100644 utils/errors.js create mode 100644 utils/hdnode.js create mode 100644 utils/hmac.js create mode 100644 utils/id.js create mode 100644 utils/index.js create mode 100644 utils/keccak256.js create mode 100644 utils/namehash.js create mode 100644 utils/pbkdf2.js create mode 100644 utils/properties.js create mode 100644 utils/random-bytes.js create mode 100644 utils/rlp.js create mode 100644 utils/secret-storage.js create mode 100644 utils/sha2.js create mode 100644 utils/signing-key.js create mode 100644 utils/solidity.js create mode 100644 utils/throw-error.js create mode 100644 utils/units.js create mode 100644 utils/utf8.js create mode 100644 utils/words.json diff --git a/Account.js b/Account.js new file mode 100644 index 0000000..34d02e2 --- /dev/null +++ b/Account.js @@ -0,0 +1,60 @@ +'use strict' +const Coins = {} +Coins.TIC = require('./tic.js').TIC; +Coins.ETH = require('./eth.js').ETH; +Coins.ERC20 = require('./eth.js').ERC20; +Coins.BTC = require('./btc.js').BTC; + +class Account { + constructor(coinType,privateKey,contractAddress){ + if(coinType === 'tic' || coinType === 'btc' || coinType === 'eth') + return new Coins[coinType.toUpperCase()](privateKey) + else + return new Coins.ERC20(privateKey,contractAddress) + } + static generateNewAccount(coinType){ + if(coinType === 'tic' || coinType === 'btc') + return Coins[coinType.toUpperCase()].generateNewAccount() + return Coins.ETH.generateNewAccount() + } + static fromMnemonic(coinType,mnemonic){ + if(coinType === 'tic' || coinType === 'btc' || coinType === 'eth') + return Coins[coinType.toUpperCase()].fromMnemonic(mnemonic) + return Coins.ETH.fromMnemonic(mnemonic) + } + static fromPrivateKey(coinType,privateKey,contractAddress){ + if(coinType === 'tic' || coinType === 'btc' || coinType === 'eth') + return new Coins[coinType.toUpperCase()](privateKey) + return new Coins.ERC20(privateKey,contractAddress) + } + static async fromOfficalWallet(encryptedWallet,key){ + return await Coins.ETH.fromEncryptedWallet(encryptedWallet,key) + } + static async getBalance(coinType,address,contractAddress){ + if(coinType === 'tic' || coinType === 'btc' || coinType === 'eth') + return await Coins[coinType.toUpperCase()].getBalance(address) + return await Coins.ERC20.getBalance(address,contractAddress) + } + static async getActions(coinType,address,contractAddress){ + if(coinType === 'tic' || coinType === 'btc' || coinType === 'eth') + return await Coins[coinType.toUpperCase()].getActions(address) + return await Coins.ERC20.getActions(address,contractAddress) + } + static decrypt(coinType,encryptedWallet,key){ + if(coinType === 'tic' || coinType === 'btc' || coinType === 'eth') + return Coins[coinType.toUpperCase()].decrypt(encryptedWallet,key) + return Coins.ETH.decrypt(encryptedWallet,key) + } + static isValidAddress(coinType, address){ + if(!coinType || !address) return null + switch(coinType){ + case "tic": return Coins.TIC.isValidAddress(address) + case "btc": return Coins.BTC.isValidAddress(address) + default: return Coins.ETH.isValidAddress(address) + } + } +} + +module.exports = { + Account +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..39db21e --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +--- +title: Toolset for Cryptocurrency +tags: + - bitcoin + - ethereum + - tic +--- +to start +> git clone https://github.com/chaosBreaking/BitHoole.git + +for use +> account = require('BitHoole') \ No newline at end of file diff --git a/btc.js b/btc.js new file mode 100644 index 0000000..1d3e81a --- /dev/null +++ b/btc.js @@ -0,0 +1,151 @@ +'use strict' + +const axios = require('axios'); +const HDNode = require('./utils/hdnode'); +const bitcoinjs = require('bitcoinjs-lib'); +// const bitcore = require('tic.common').Bitcore; +const ticCommon = require('tic.common').Crypto; +const BTC_NODE = require('./netConfig').BTC_NODE; +const BTC_NODE2 = require('./netConfig').BTC_NODE2; +const BTC_TXFEE = 30; + +class BTC { + constructor(privateKey){ + if(!ticCommon.isSeckey(privateKey)) throw new Error('Invalid PrivateKey') + var publicKey = ticCommon.seckey2pubkey(privateKey) + Object.defineProperties(this,{ + "privateKey" : { + enumerable : true, + writable : false, + value : privateKey + }, + "publicKey": { + enumerable : true, + writable : false, + value : ticCommon.seckey2pubkey(privateKey,{coin:"BTC"}) + }, + "address" : { + enumerable : true, + writable : false, + value : ticCommon.pubkey2address(publicKey,{coin:"BTC"}) + }, + "url" : { + enumerable : true, + get: function() { return this._url; }, + set: function(url) { + if (typeof(url) !== 'string') { throw new Error('invalid url'); } + this._url = url; + } + }, + "defaultGas":{ + enumerable: true, + get: function() { return this._defaultGasFee; }, + set: function(value) { + if (typeof(value) !== 'number') { throw new Error('invalid defaultGasFee'); } + this._defaultGasFee = value; + } + } + }) + this._url = BTC_NODE; + this._defaultGasFee = BTC_TXFEE; + + } + static generateNewAccount(){ + var mnemonic = ticCommon.randomSecword() + return Object.assign(new BTC(ticCommon.secword2keypair(mnemonic, {coin:"BTC"}).seckey),{mnemonic : mnemonic}) + } + static fromMnemonic(mnemonic){ + HDNode.isValidMnemonic(mnemonic) + return Object.assign(new BTC(ticCommon.secword2keypair(mnemonic, {coin:"BTC"}).seckey),{mnemonic:mnemonic}) + } + static async getBalance(address){ + return (await axios.get(`${BTC_NODE}/addrs/${address}/balance`)).data.balance + } + static async getActions(address){ + return (await axios.get(`${BTC_NODE}/addrs/${address}`)).data.txrefs + } + static async getUTXO(address){ + // console.log(`${BTC_NODE2}/unspent?active=${address}`,`${BTC_NODE2}/unspent?active=${address}`) + try { + return (await axios.get(`${BTC_NODE2}/unspent?active=${address}`)).data.unspent_outputs + } catch (error) { + return null + } + } + static encrypt(data, key){ + if(!data || !key) throw new Error('Required Params Missing') + return ticCommon.encrypt(data,key) + } + static decrypt(data, key){ + return ticCommon.decrypt(data, key, {format:"json"}) //return null for wrong key + } + static isValidAddress(address){ + return address.length == 34 && address[0] == '1' + } + async sendTransaction(toAddress, amount, option = {gasFee : BTC_TXFEE}){ + let set = bitcoinjs.ECPair.fromPrivateKey(Buffer.from(this.privateKey,'hex'));//导入私钥用于签名 + let txb = new bitcoinjs.TransactionBuilder();//初始化交易对象 + let tx = await BTC.getUTXO('1DEP8i3QJCsomS4BSMY2RpU1upv62aGvhD') + if(!tx) return null + var tot = 0;//用于记录UTXO总量 + amount+=1e4;//消费金额是转出金额加上10000的矿工费 + txb.setVersion(1);//设置交易版本号 + for(var i=0;i/0'/0/0 + var mnemonic = ticCommon.randomSecword(); + return Object.assign(ETH.fromMnemonic(mnemonic, option),{mnemonic,mnemonic}) + } + static fromMnemonic(mnemonic, option = {path:defaultPath}){ + HDNode.isValidMnemonic(mnemonic) //check valid mnemonic,will throw Error if not valid + let seed = HDNode.mnemonicToSeed(mnemonic) + return new ETH(HDNode.fromSeed(seed).derivePath(option.path).privateKey) + } + static async getBalance(address){ + if(!address){ throw new Error('Address is required'); } + let res = (await axios.post(ETH_NODE,{ + "jsonrpc":"2.0","method":"eth_getBalance","params":[address, "latest"],"id":1 + })).data + if(res) + return parseInt(res.result)/1e18 //1000000000000000000 + else return null + } + static async getActions(address){ + let tx = await eth.account.txlist(address, 0 ,'latast') + if(tx && tx.message === "OK") + return tx.result + else return [] + } + static fromEncryptedWallet(json, password, progressCallback) { + if (progressCallback && typeof(progressCallback) !== 'function') { + throw new Error('invalid callback'); + } + + return new Promise(function(resolve, reject) { + + if (secretStorage.isCrowdsaleWallet(json)) { + try { + var privateKey = secretStorage.decryptCrowdsale(json, password); + resolve(new ETH(privateKey)); + } catch (error) { + reject(error); + } + + } else if (secretStorage.isValidWallet(json)) { + + secretStorage.decrypt(json, password, progressCallback).then(function(signingKey) { + var wallet = new ETH(signingKey); + if (signingKey.mnemonic && signingKey.path) { + utils.defineProperty(wallet, 'mnemonic', signingKey.mnemonic); + utils.defineProperty(wallet, 'path', signingKey.path); + } + resolve(wallet); + return null; + }, function(error) { + reject(error); + }).catch(function(error) { reject(error); }); + + } else { + reject('invalid wallet JSON'); + } + }); + } + static parseTransaction(rawTransaction){ + rawTransaction = utils.hexlify(rawTransaction, 'rawTransaction'); + var signedTransaction = utils.RLP.decode(rawTransaction); + if (signedTransaction.length !== 9) { throw new Error('invalid transaction'); } + + var raw = []; + + var transaction = {}; + transactionFields.forEach(function(fieldInfo, index) { + transaction[fieldInfo.name] = signedTransaction[index]; + raw.push(signedTransaction[index]); + }); + + if (transaction.to) { + if (transaction.to == '0x') { + delete transaction.to; + } else { + transaction.to = utils.getAddress(transaction.to); + } + } + + ['gasPrice', 'gasLimit', 'nonce', 'value'].forEach(function(name) { + if (!transaction[name]) { return; } + if (transaction[name].length === 0) { + transaction[name] = utils.bigNumberify(0); + } else { + transaction[name] = utils.bigNumberify(transaction[name]); + } + }); + + if (transaction.nonce) { + transaction.nonce = transaction.nonce.toNumber(); + } else { + transaction.nonce = 0; + } + + var v = utils.arrayify(signedTransaction[6]); + var r = utils.arrayify(signedTransaction[7]); + var s = utils.arrayify(signedTransaction[8]); + + if (v.length >= 1 && r.length >= 1 && r.length <= 32 && s.length >= 1 && s.length <= 32) { + transaction.v = utils.bigNumberify(v).toNumber(); + transaction.r = signedTransaction[7]; + transaction.s = signedTransaction[8]; + + var chainId = (transaction.v - 35) / 2; + if (chainId < 0) { chainId = 0; } + chainId = parseInt(chainId); + + transaction.chainId = chainId; + + var recoveryParam = transaction.v - 27; + + if (chainId) { + raw.push(utils.hexlify(chainId)); + raw.push('0x'); + raw.push('0x'); + recoveryParam -= chainId * 2 + 8; + } + + var digest = utils.keccak256(utils.RLP.encode(raw)); + try { + transaction.from = SigningKey.recover(digest, r, s, recoveryParam); + } catch (error) { + console.log(error); + } + } + + + return transaction; + } + static encrypt(data, key){ + if(!data || !key) throw new Error('Required Params Missing') + return ticCommon.encrypt(data,key) + } + static decrypt(data, key){ + return ticCommon.decrypt(data, key, {format:"json"}) //return null for wrong key + } + static async estimateGasPrice(){ + try{ + return parseInt((await axios.post(ETH_NODE, { + "method": "eth_gasPrice", + "id": "6842", + "jsonrpc": "2.0" + })).data.result)/1e9 + } + catch(err){ + return 1 + } + } + + static isValidAddress(address){ + let res = address.match(/^(0x)?[0-9a-fA-F]{40}$/) + return res && res[0].slice(0,2) === '0x' + } + + async getBalance(){ + return ETH.getBalance(this.address) + } + async getActions(){ + return ETH.getActions(this.address) + } + async getTransactionCount(){ + if(!this._url){ throw new Error('Base url required'); } + var self = this; + return (await axios.post(this._url,{ + "jsonrpc":"2.0","method":"eth_getTransactionCount","params":[self.address, "latest"],"id":1 + })).data.result||null + } + signTransaction(transaction){ + var chainId = transaction.chainId; + if (chainId == null && this.provider) { chainId = this.provider.chainId; } + if (!chainId) { chainId = 0; } + + var raw = []; + transactionFields.forEach(function(fieldInfo) { + var value = transaction[fieldInfo.name] || ([]); + value = utils.arrayify(utils.hexlify(value), fieldInfo.name); + + // Fixed-width field + if (fieldInfo.length && value.length !== fieldInfo.length && value.length > 0) { + var error = new Error('invalid ' + fieldInfo.name); + error.reason = 'wrong length'; + error.value = value; + throw error; + } + + // Variable-width (with a maximum) + if (fieldInfo.maxLength) { + value = utils.stripZeros(value); + if (value.length > fieldInfo.maxLength) { + var error = new Error('invalid ' + fieldInfo.name); + error.reason = 'too long'; + error.value = value; + throw error; + } + } + + raw.push(utils.hexlify(value)); + }); + + if (chainId) { + raw.push(utils.hexlify(chainId)); + raw.push('0x'); + raw.push('0x'); + } + + var digest = utils.keccak256(utils.RLP.encode(raw)); + var signingKey = new SigningKey(this.privateKey); + var signature = signingKey.signDigest(digest); + + var v = 27 + signature.recoveryParam + if (chainId) { + raw.pop(); + raw.pop(); + raw.pop(); + v += chainId * 2 + 8; + } + + raw.push(utils.hexlify(v)); + raw.push(utils.stripZeros(utils.arrayify(signature.r))); + raw.push(utils.stripZeros(utils.arrayify(signature.s))); + + return utils.RLP.encode(raw); + } + async sendTransaction(toAddress, amount, option = {gasFee : GAS_Fee}){ + /**************************************************************** + 1 Ether = 1e18 wei + 1Gwei = 1e9 wei + *GWei as the unit of gasPrice, minimum gasPrice is 1Gwei + *unit of amount is ether,should be translate to wei + ****************************************************************/ + let nonce = await this.getTransactionCount(); + if(!nonce) nonce = '0x0' + var gasPrice, gasLimit; + if(!option.gasPrice || !option.gasLimit){ + //Normal Mode:use customized gasFee( ether ) to caculate gasPrice( wei ), gasLimit use default value + gasLimit = GAS_LIMIT; + gasPrice = String(option.gasFee * GAS_UNIT_WEI / gasLimit); + } + else{ + //Advance Mode:specified the gasLimit and gasPrice( gwei ) + gasLimit = option.gasLimit; + gasPrice = String(GAS_UNIT_GWEI * option.gasPrice) + } + let transaction = { + nonce: nonce, + gasLimit: gasLimit, + gasPrice: utils.bigNumberify(gasPrice), + to: toAddress, + + value: utils.parseEther(String(amount)), + }; + try{ + let signedTransaction = this.signTransaction(transaction); + let ethTxRes = (await axios.post(ETH_NODE,{ + "jsonrpc":"2.0", + "method":"eth_sendRawTransaction", + "params":[signedTransaction.toString('hex')], + "id":6842 + })).data + if(ethTxRes && ethTxRes.result) + return ethTxRes + return null + } + catch(err){ + return null + } + } + encrypt(key){ + return ETH.encrypt(this, key) + } +} + +class ERC20 extends ETH{ + constructor(privateKey, contractAddress){ + if(!contractAddress) throw new Error('Missing contractAddress') + super(privateKey); + Object.defineProperty(this, 'contractAddress',{ + enumerable:true, + writable:false, + value:contractAddress + }) + } + static async getDecimals(contractAddress){ + if(!contractAddress) throw new Error('Missing params') + let queryAddress = '0x313ce567' + (contractAddress.split('x')[1]).padStart(64,'0') + let params = [{"to":contractAddress, "data":queryAddress},"latest"] + let queryData = { + "jsonrpc":"2.0", + "method":"eth_call", + "params":params, + "id":6842 + } + return parseInt((await axios.post(ETH_NODE, queryData)).data.result) + } + static async getBalance(address, contractAddress){ + if(!address || !contractAddress) throw new Error('Missing params') + let queryAddress = '0x70a08231' + (address.split('x')[1]).padStart(64,'0') + let params = [{"to":contractAddress, "data":queryAddress},"latest"] + let queryData = { + "jsonrpc":"2.0", + "method":"eth_call", + "params":params, + "id":6842 + } + // return parseInt(erc20res.result)/Number('10'.padEnd(ERC20Table[obj.name].decimals+1,'0')) + let res = (await axios.post(ETH_NODE, queryData)).data.result + if(res == '0x') return 0 + return parseInt(res) + } + static async getActions(address, contractAddress){ + try{ + let res = (await eth.account.tokentx(address,contractAddress)) + if(res && res.result) + return res.result + } + catch(err){ + return [] + } + return + } + + async getBalance(){ + return ERC20.getBalance(this.address, this.contractAddress) + } + async getActions(){ + return ERC20.getActions(this.address, this.contractAddress) + } + async getDecimals(){ + let decimals = await ERC20.getDecimals(this.contractAddress) + if(decimals) + Object.defineProperty(this, 'decimals', { + enumerable:true, + value:decimals, + writable:false + }) + else + return 0 // any good idea? + } + async sendTransaction(toAddress, amount, option = {gasFee : GAS_Fee_ERC20}){ + /**************************************************************** + 1 Ether = 1e18 wei + 1 Gwei = 1e9 wei + *GWei as the unit of gasPrice, minimum gasPrice is 1Gwei + minimum gaslimit for erc20transaction is 60000 + ****************************************************************/ + var nonce = await this.getTransactionCount(); + var gasPrice, gasLimit, decimals, contractAddress = this.contractAddress; + if(!nonce) nonce = '0x0' + if(!option.gasPrice || !option.gasLimit){ + //Normal Mode:use customized gasFee( ether ) to caculate gasPrice( wei ), gasLimit use default value + gasLimit = GAS_LIMIT_ERC20; + gasPrice = String(option.gasFee * GAS_UNIT_WEI / gasLimit); + } + else{ + //Advance Mode:specified the gasLimit and gasPrice( gwei ) + gasLimit = option.gasLimit; + gasPrice = String(GAS_UNIT_GWEI * option.gasPrice) + } + if(!option.decimals) decimals = await ERC20.getDecimals(contractAddress) + let txBody = '0x' + 'a9059cbb' + toAddress.split('x')[1].padStart(64,'0')+ Number(amount*Math.pow(10,decimals)).toString(16).padStart(64,'0') + let transaction = { + nonce: nonce, + gasLimit: gasLimit, + gasPrice : utils.bigNumberify(gasPrice), + to: contractAddress, + value : 0, + data : txBody + }; + let signedTransaction = this.signTransaction(transaction); + try{ + let erc20TxRes = (await axios.post(ETH_NODE, { + "jsonrpc":"2.0", + "method":"eth_sendRawTransaction", + "params":[signedTransaction.toString('hex')], + "id":6842 + })).data + if(erc20TxRes && erc20TxRes.result) + return erc20TxRes.result + console.log(erc20TxRes) + return null + } + catch(err){ + return null + } + } + +} + +module.exports = { + ETH, ERC20 +} + diff --git a/index.js b/index.js new file mode 100644 index 0000000..cb7839d --- /dev/null +++ b/index.js @@ -0,0 +1,16 @@ +'use strict' + +const TIC = require('./tic.js').TIC; +const ETH = require('./eth.js').ETH; +const ERC20 = require('./eth.js').ERC20; +const BTC = require('./btc.js').BTC; +const Account = require('./Account').Account; +const Crypto = require('tic.common').Crypto; +module.exports = { + TIC, + ETH, + BTC, + ERC20, + Account, + Crypto, +} \ No newline at end of file diff --git a/netConfig.js b/netConfig.js new file mode 100644 index 0000000..7e92455 --- /dev/null +++ b/netConfig.js @@ -0,0 +1,14 @@ + +const TIC_NODE = 'https://bank.bittic.net:7285/api'; +const BTC_NODE = 'https://api.blockcypher.com/v1/btc/main'; +const BTC_NODE2 = 'https://blockchain.info'//https://blockchain.info/unspent?active=12HnmPpLomtPL53Q4s6xEqRB4wkMHi5GEZ +const ETH_NODE = 'https://mainnet.infura.io/8284219b092f4cc69f3de29e532b1eb2'; +const ETH_NODE2 = 'https://api.myetherapi.com/eth'; +const ETH_TEST_NODE = 'https://ropsten.infura.io/8284219b092f4cc69f3de29e532b1eb2'; + +module.exports = { + TIC_NODE, + ETH_NODE, + BTC_NODE, + BTC_NODE2, +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..856efdd --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "tic.common", + "version": "1.2.1", + "dependencies": { + "aes-js": "^3.1.1", + "axios": "^0.18.0", + "bitcoinjs-lib": "^4.0.2", + "elliptic": "^6.4.1", + "etherscan-api": "^8.1.3", + "js-sha3": "^0.8.0", + "scrypt-js": "^2.0.3", + "setimmediate": "^1.0.5", + "tic.common": "git+https://git.faronear.org/tic/tic.common.git", + "uuid": "^3.3.2" + }, + "deprecated": false, + "description": "blockchain tool for ticwallet", + "keywords": [ + "tool", + "blockchain", + "tic" + ], + "main": "index.js" +} diff --git a/tic.js b/tic.js new file mode 100644 index 0000000..ed47031 --- /dev/null +++ b/tic.js @@ -0,0 +1,136 @@ +'use strict' +const axios = require('axios') +const ticCommon = require('tic.common').Crypto +const ticActTransfer = require('tic.common').ActTransfer + +const TIC_TXFEE = 10; +const TIC_NODE = require('./netConfig').TIC_NODE + +class TIC { + constructor(seckey,option={}){ + if(!seckey||!ticCommon.isSeckey(seckey)) throw "ERROR:Invalid Seckey" + Object.defineProperties(this, { + 'seckey' : { + value : seckey, + enumerable : true, + writable : false, + }, + 'pubkey' : { + value : ticCommon.seckey2pubkey(seckey), + enumerable : true, + writable : false, + }, + 'address' : { + value : ticCommon.pubkey2address(ticCommon.seckey2pubkey(seckey)), + enumerable : true, + writable : false + } + }) + Object.assign(this,{ + _url : option._url||TIC_NODE, + _defaultFee : option.fee||TIC_TXFEE //fee cannot be zero + }) + } + get url(){return this._url} + set url(newURL){this._url = newURL} + get txfee(){return this._defaultFee} + set txfee(fee){this._defaultFee = fee} + + static generateNewAccount(){ + var secword = ticCommon.randomSecword() + return Object.assign(new TIC(ticCommon.secword2keypair(secword).seckey),{secword:secword}) + } + static fromMnemonic(secword){ + if(!secword||!ticCommon.isSecword(secword)) throw "ERROR:Invalid Secword" + return new TIC(ticCommon.secword2keypair(secword).seckey) + } + static async getBalance(address){ + if(!address){ throw new Error('Address is required'); } + return (await axios.post(TIC_NODE+'/Account/getBalance',{ + "Account" : { + "address":address + } + })).data + } + static async getActions(address){ + if(!address){ throw new Error('Address is required'); } + return (await axios.post(TIC_NODE+'/Action/getActionList',{ + "Action" : { + "actorAddress" : address, + "toAddress" : address + }, + "config":{ + "logic":"OR" + } + })).data + } + static encrypt(data, key){ + if(!data || !key) throw new Error('Required Params Missing') + return ticCommon.encrypt(data,key) + } + static decrypt(data, key){ + return ticCommon.decrypt(data, key, {format:"json"}) //return null for wrong key + } + + static isValidAddress(address){ + return ticCommon.isAddress(address) + } + + async sendTransaction(toAddress, amount, option = {gasFee : TIC_TXFEE}){ + if(!toAddress||!amount){throw new Error("ERROR:RequiredParamsMissing")} //amount cannot be zero + let action = new ticActTransfer({ + amount: parseInt(amount), + toAddress: toAddress, + fee: option.gasFee + }) + //对交易数据签名,packMe 内的参数是交易发起人的keypair + action.packMe({ + seckey: this.seckey, + pubkey: this.pubkey, + address: this.address + }) + let data = { + Action:action + } + try{ + + let res = (await axios.post(this._url + '/Action/prepare',data)).data + return res + }catch(err){ + return null + } + } + async getBalance(){ + return TIC.getBalance(this.address) + } + async getActions(){ + return TIC.getActions(this.address) + } + getSerializedTx(option){ + if(!option.toAddress||!option.amount){throw new Error("ERROR:RequiredParamsMissing")} + let action=new ticActTransfer({ + amount: parseInt(option.amount), + toAddress: option.toAddress, + fee:option.fee||this._defaultFee + }) + //sign for txBody use function packMe, which needs actor's keypair as parameter + action.packMe({ + seckey: this.seckey, + pubkey: this.pubkey, + address: this.address + }) + return action + } + //default key for sign&encrypt is account's seckey,other keys are optional. + sign(message,key = this.seckey){ + return ticCommon.sign(message,key) + } + verify(message,signature){ + return ticCommon.sign(message,signature,this.seckey) + } + encrypt(key){ + return TIC.encrypt(this, key) + } + +} +module.exports = {TIC} diff --git a/util.js b/util.js new file mode 100644 index 0000000..c5453f5 --- /dev/null +++ b/util.js @@ -0,0 +1,35 @@ +'use strict' +module.exports = (function() { + var convert = require('./utils/convert'); + var hmac = require('./utils/hmac'); + var base64 = require('./utils/base64'); + return { + defineProperty: require('./utils/properties').defineProperty, + + arrayify: convert.arrayify, + hexlify: convert.hexlify, + stripZeros: convert.stripZeros, + concat: convert.concat, + padZeros: convert.padZeros, + stripZeros: convert.stripZeros, + base64: base64, + + bigNumberify: require('./utils/bignumber').bigNumberify, + + toUtf8Bytes: require('./utils/utf8').toUtf8Bytes, + + getAddress: require('./utils/address').getAddress, + + keccak256: require('./utils/keccak256'), + + RLP: require('./utils/rlp'), + + pbkdf2: require('./utils/pbkdf2.js'), + + createSha512Hmac: hmac.createSha512Hmac, + + // isMnemonic: isMnemonic, + + parseEther:require('./utils/units').parseEther + }; +})(); diff --git a/utils/abi-coder.js b/utils/abi-coder.js new file mode 100644 index 0000000..a029aa8 --- /dev/null +++ b/utils/abi-coder.js @@ -0,0 +1,1018 @@ +'use strict'; + +// See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI + +var utils = (function() { + var convert = require('../utils/convert.js'); + var utf8 = require('../utils/utf8.js'); + + return { + defineProperty: require('../utils/properties.js').defineProperty, + + arrayify: convert.arrayify, + padZeros: convert.padZeros, + + bigNumberify: require('../utils/bignumber.js').bigNumberify, + + getAddress: require('../utils/address').getAddress, + + concat: convert.concat, + + toUtf8Bytes: utf8.toUtf8Bytes, + toUtf8String: utf8.toUtf8String, + + hexlify: convert.hexlify, + }; +})(); + +var errors = require('./errors'); + +var paramTypeBytes = new RegExp(/^bytes([0-9]*)$/); +var paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/); +var paramTypeArray = new RegExp(/^(.*)\[([0-9]*)\]$/); + +var defaultCoerceFunc = function(type, value) { + var match = type.match(paramTypeNumber) + if (match && parseInt(match[2]) <= 48) { return value.toNumber(); } + return value; +} + +// Shallow copy object (will move to utils/properties in v4) +function shallowCopy(object) { + var result = {}; + for (var key in object) { result[key] = object[key]; } + return result; +} + +/////////////////////////////////// +// Parsing for Solidity Signatures + +var regexParen = new RegExp("^([^)(]*)\\((.*)\\)([^)(]*)$"); +var regexIdentifier = new RegExp("^[A-Za-z_][A-Za-z0-9_]*$"); + +var close = { "(": ")", "[": "]" }; + +function verifyType(type) { + + // These need to be transformed to their full description + if (type.match(/^uint($|[^1-9])/)) { + type = 'uint256' + type.substring(4); + } else if (type.match(/^int($|[^1-9])/)) { + type = 'int256' + type.substring(3); + } + + return type; +} + +function parseParam(param, allowIndexed) { + function throwError(i) { + throw new Error('unexpected character "' + param[i] + '" at position ' + i + ' in "' + param + '"'); + } + + var parent = { type: '', name: '', state: { allowType: true } }; + var node = parent; + + for (var i = 0; i < param.length; i++) { + var c = param[i]; + switch (c) { + case '(': + if (!node.state.allowParams) { throwError(i); } + delete node.state.allowType; + node.type = verifyType(node.type); + node.components = [ { type: '', name: '', parent: node, state: { allowType: true } } ]; + node = node.components[0]; + break; + + case ')': + delete node.state; + node.type = verifyType(node.type); + + var child = node; + node = node.parent; + if (!node) { throwError(i); } + delete child.parent; + delete node.state.allowParams; + node.state.allowName = true; + node.state.allowArray = true; + break; + + case ',': + delete node.state; + node.type = verifyType(node.type); + + var sibling = { type: '', name: '', parent: node.parent, state: { allowType: true } }; + node.parent.components.push(sibling); + delete node.parent; + node = sibling; + break; + + // Hit a space... + case ' ': + + // If reading type, the type is done and may read a param or name + if (node.state.allowType) { + if (node.type !== '') { + node.type = verifyType(node.type); + delete node.state.allowType; + node.state.allowName = true; + node.state.allowParams = true; + } + } + + // If reading name, the name is done + if (node.state.allowName) { + if (node.name !== '') { + if (allowIndexed && node.name === 'indexed') { + node.indexed = true; + node.name = ''; + } else { + delete node.state.allowName; + } + } + } + + break; + + case '[': + if (!node.state.allowArray) { throwError(i); } + + //if (!node.array) { node.array = ''; } + //node.array += c; + node.type += c; + + delete node.state.allowArray; + delete node.state.allowName; + node.state.readArray = true; + break; + + case ']': + if (!node.state.readArray) { throwError(i); } + + //node.array += c; + node.type += c; + + delete node.state.readArray; + node.state.allowArray = true; + node.state.allowName = true; + break; + + default: + if (node.state.allowType) { + node.type += c; + node.state.allowParams = true; + node.state.allowArray = true; + } else if (node.state.allowName) { + node.name += c; + delete node.state.allowArray; + } else if (node.state.readArray) { + //node.array += c; + node.type += c; + } else { + throwError(i); + } + } + } + + if (node.parent) { throw new Error("unexpected eof"); } + + delete parent.state; + parent.type = verifyType(parent.type); + + //verifyType(parent); + + return parent; +} + +function parseSignatureEvent(fragment) { + + var abi = { + anonymous: false, + inputs: [], + type: 'event' + } + + var match = fragment.match(regexParen); + if (!match) { throw new Error('invalid event: ' + fragment); } + + abi.name = match[1].trim(); + + splitNesting(match[2]).forEach(function(param) { + param = parseParam(param, true); + param.indexed = !!param.indexed; + abi.inputs.push(param); + }); + + match[3].split(' ').forEach(function(modifier) { + switch(modifier) { + case 'anonymous': + abi.anonymous = true; + break; + case '': + break; + default: + console.log('unknown modifier: ' + mdifier); + } + }); + + if (abi.name && !abi.name.match(regexIdentifier)) { + throw new Error('invalid identifier: "' + result.name + '"'); + } + + return abi; +} + +function parseSignatureFunction(fragment) { + var abi = { + constant: false, + inputs: [], + outputs: [], + payable: false, + type: 'function' + }; + + var comps = fragment.split(' returns '); + var left = comps[0].match(regexParen); + if (!left) { throw new Error('invalid signature'); } + + abi.name = left[1].trim(); + if (!abi.name.match(regexIdentifier)) { + throw new Error('invalid identifier: "' + left[1] + '"'); + } + + splitNesting(left[2]).forEach(function(param) { + abi.inputs.push(parseParam(param)); + }); + + left[3].split(' ').forEach(function(modifier) { + switch (modifier) { + case 'constant': + abi.constant = true; + break; + case 'payable': + abi.payable = true; + break; + case 'pure': + abi.constant = true; + abi.stateMutability = 'pure'; + break; + case 'view': + abi.constant = true; + abi.stateMutability = 'view'; + break; + case '': + break; + default: + console.log('unknown modifier: ' + modifier); + } + }); + + // We have outputs + if (comps.length > 1) { + var right = comps[1].match(regexParen); + if (right[1].trim() != '' || right[3].trim() != '') { + throw new Error('unexpected tokens'); + } + + splitNesting(right[2]).forEach(function(param) { + abi.outputs.push(parseParam(param)); + }); + } + + return abi; +} + + +function parseSignature(fragment) { + if(typeof(fragment) === 'string') { + // Make sure the "returns" is surrounded by a space and all whitespace is exactly one space + fragment = fragment.replace(/\(/g, ' (').replace(/\)/g, ') ').replace(/\s+/g, ' '); + fragment = fragment.trim(); + + if (fragment.substring(0, 6) === 'event ') { + return parseSignatureEvent(fragment.substring(6).trim()); + + } else { + if (fragment.substring(0, 9) === 'function ') { + fragment = fragment.substring(9); + } + return parseSignatureFunction(fragment.trim()); + } + } + + throw new Error('unknown fragment'); +} + + +/////////////////////////////////// +// Coders + +var coderNull = function(coerceFunc) { + return { + name: 'null', + type: '', + encode: function(value) { + return utils.arrayify([]); + }, + decode: function(data, offset) { + if (offset > data.length) { throw new Error('invalid null'); } + return { + consumed: 0, + value: coerceFunc('null', undefined) + } + }, + dynamic: false + }; +} + +var coderNumber = function(coerceFunc, size, signed, localName) { + var name = ((signed ? 'int': 'uint') + (size * 8)); + return { + localName: localName, + name: name, + type: name, + encode: function(value) { + try { + value = utils.bigNumberify(value) + } catch (error) { + errors.throwError('invalid number value', errors.INVALID_ARGUMENT, { + arg: localName, + type: typeof(value), + value: value + }); + } + value = value.toTwos(size * 8).maskn(size * 8); + + if (signed) { + value = value.fromTwos(size * 8).toTwos(256); + } + + return utils.padZeros(utils.arrayify(value), 32); + }, + decode: function(data, offset) { + if (data.length < offset + 32) { + errors.throwError('insufficient data for ' + name + ' type', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: name, + value: utils.hexlify(data.slice(offset, offset + 32)) + }); + } + var junkLength = 32 - size; + var value = utils.bigNumberify(data.slice(offset + junkLength, offset + 32)); + if (signed) { + value = value.fromTwos(size * 8); + } else { + value = value.maskn(size * 8); + } + + //if (size <= 6) { value = value.toNumber(); } + + return { + consumed: 32, + value: coerceFunc(name, value), + } + } + }; +} +var uint256Coder = coderNumber(function(type, value) { return value; }, 32, false); + +var coderBoolean = function(coerceFunc, localName) { + return { + localName: localName, + name: 'bool', + type: 'bool', + encode: function(value) { + return uint256Coder.encode(!!value ? 1: 0); + }, + decode: function(data, offset) { + try { + var result = uint256Coder.decode(data, offset); + } catch (error) { + if (error.reason === 'insufficient data for uint256 type') { + errors.throwError('insufficient data for boolean type', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'boolean', + value: error.value + }); + } + throw error; + } + return { + consumed: result.consumed, + value: coerceFunc('boolean', !result.value.isZero()) + } + } + } +} + +var coderFixedBytes = function(coerceFunc, length, localName) { + var name = ('bytes' + length); + return { + localName: localName, + name: name, + type: name, + encode: function(value) { + try { + value = utils.arrayify(value); + + // @TODO: In next major change, the value.length MUST equal the + // length, but that is a backward-incompatible change, so here + // we just check for things that can cause problems. + if (value.length > 32) { + throw new Error('too many bytes for field'); + } + + } catch (error) { + errors.throwError('invalid ' + name + ' value', errors.INVALID_ARGUMENT, { + arg: localName, + type: typeof(value), + value: error.value + }); + } + + if (value.length === 32) { return value; } + + var result = new Uint8Array(32); + result.set(value); + return result; + }, + decode: function(data, offset) { + if (data.length < offset + 32) { + errors.throwError('insufficient data for ' + name + ' type', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: name, + value: utils.hexlify(data.slice(offset, offset + 32)) + }); + } + + return { + consumed: 32, + value: coerceFunc(name, utils.hexlify(data.slice(offset, offset + length))) + } + } + }; +} + +var coderAddress = function(coerceFunc, localName) { + return { + localName: localName, + name: 'address', + type: 'address', + encode: function(value) { + try { + value = utils.arrayify(utils.getAddress(value)); + } catch (error) { + errors.throwError('invalid address', errors.INVALID_ARGUMENT, { + arg: localName, + type: typeof(value), + value: value + }); + } + var result = new Uint8Array(32); + result.set(value, 12); + return result; + }, + decode: function(data, offset) { + if (data.length < offset + 32) { + errors.throwError('insufficuent data for address type', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'address', + value: utils.hexlify(data.slice(offset, offset + 32)) + }); + } + return { + consumed: 32, + value: coerceFunc('address', utils.getAddress(utils.hexlify(data.slice(offset + 12, offset + 32)))) + } + } + } +} + +function _encodeDynamicBytes(value) { + var dataLength = parseInt(32 * Math.ceil(value.length / 32)); + var padding = new Uint8Array(dataLength - value.length); + + return utils.concat([ + uint256Coder.encode(value.length), + value, + padding + ]); +} + +function _decodeDynamicBytes(data, offset, localName) { + if (data.length < offset + 32) { + errors.throwError('insufficient data for dynamicBytes length', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'dynamicBytes', + value: utils.hexlify(data.slice(offset, offset + 32)) + }); + } + + var length = uint256Coder.decode(data, offset).value; + try { + length = length.toNumber(); + } catch (error) { + errors.throwError('dynamic bytes count too large', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'dynamicBytes', + value: length.toString() + }); + } + + if (data.length < offset + 32 + length) { + errors.throwError('insufficient data for dynamicBytes type', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'dynamicBytes', + value: utils.hexlify(data.slice(offset, offset + 32 + length)) + }); + } + + return { + consumed: parseInt(32 + 32 * Math.ceil(length / 32)), + value: data.slice(offset + 32, offset + 32 + length), + } +} + +var coderDynamicBytes = function(coerceFunc, localName) { + return { + localName: localName, + name: 'bytes', + type: 'bytes', + encode: function(value) { + try { + value = utils.arrayify(value); + } catch (error) { + errors.throwError('invalid bytes value', errors.INVALID_ARGUMENT, { + arg: localName, + type: typeof(value), + value: error.value + }); + } + return _encodeDynamicBytes(value); + }, + decode: function(data, offset) { + var result = _decodeDynamicBytes(data, offset, localName); + result.value = coerceFunc('bytes', utils.hexlify(result.value)); + return result; + }, + dynamic: true + }; +} + +var coderString = function(coerceFunc, localName) { + return { + localName: localName, + name: 'string', + type: 'string', + encode: function(value) { + if (typeof(value) !== 'string') { + errors.throwError('invalid string value', errors.INVALID_ARGUMENT, { + arg: localName, + type: typeof(value), + value: value + }); + } + return _encodeDynamicBytes(utils.toUtf8Bytes(value)); + }, + decode: function(data, offset) { + var result = _decodeDynamicBytes(data, offset, localName); + result.value = coerceFunc('string', utils.toUtf8String(result.value)); + return result; + }, + dynamic: true + }; +} + +function alignSize(size) { + return parseInt(32 * Math.ceil(size / 32)); +} + +function pack(coders, values) { + + if (Array.isArray(values)) { + // do nothing + + } else if (values && typeof(values) === 'object') { + var arrayValues = []; + coders.forEach(function(coder) { + arrayValues.push(values[coder.localName]); + }); + values = arrayValues; + + } else { + errors.throwError('invalid tuple value', errors.INVALID_ARGUMENT, { + coderType: 'tuple', + type: typeof(values), + value: values + }); + } + + if (coders.length !== values.length) { + errors.throwError('types/value length mismatch', errors.INVALID_ARGUMENT, { + coderType: 'tuple', + value: values + }); + } + + var parts = []; + + coders.forEach(function(coder, index) { + parts.push({ dynamic: coder.dynamic, value: coder.encode(values[index]) }); + }); + + var staticSize = 0, dynamicSize = 0; + parts.forEach(function(part, index) { + if (part.dynamic) { + staticSize += 32; + dynamicSize += alignSize(part.value.length); + } else { + staticSize += alignSize(part.value.length); + } + }); + + var offset = 0, dynamicOffset = staticSize; + var data = new Uint8Array(staticSize + dynamicSize); + + parts.forEach(function(part, index) { + if (part.dynamic) { + //uint256Coder.encode(dynamicOffset).copy(data, offset); + data.set(uint256Coder.encode(dynamicOffset), offset); + offset += 32; + + //part.value.copy(data, dynamicOffset); @TODO + data.set(part.value, dynamicOffset); + dynamicOffset += alignSize(part.value.length); + } else { + //part.value.copy(data, offset); @TODO + data.set(part.value, offset); + offset += alignSize(part.value.length); + } + }); + + return data; +} + +function unpack(coders, data, offset) { + var baseOffset = offset; + var consumed = 0; + var value = []; + coders.forEach(function(coder) { + if (coder.dynamic) { + var dynamicOffset = uint256Coder.decode(data, offset); + var result = coder.decode(data, baseOffset + dynamicOffset.value.toNumber()); + // The dynamic part is leap-frogged somewhere else; doesn't count towards size + result.consumed = dynamicOffset.consumed; + } else { + var result = coder.decode(data, offset); + } + + if (result.value != undefined) { + value.push(result.value); + } + + offset += result.consumed; + consumed += result.consumed; + }); + + coders.forEach(function(coder, index) { + var name = coder.localName; + if (!name) { return; } + + if (typeof(name) === 'object') { name = name.name; } + if (!name) { return; } + + if (name === 'length') { name = '_length'; } + + if (value[name] != null) { return; } + + value[name] = value[index]; + }); + + return { + value: value, + consumed: consumed + } + + return result; +} + +function coderArray(coerceFunc, coder, length, localName) { + var type = (coder.type + '[' + (length >= 0 ? length: '') + ']'); + + return { + coder: coder, + localName: localName, + length: length, + name: 'array', + type: type, + encode: function(value) { + if (!Array.isArray(value)) { + errors.throwError('expected array value', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'array', + type: typeof(value), + value: value + }); + } + + var count = length; + + var result = new Uint8Array(0); + if (count === -1) { + count = value.length; + result = uint256Coder.encode(count); + } + + if (count !== value.length) { + error.throwError('array value length mismatch', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'array', + count: value.length, + expectedCount: count, + value: value + }); + } + + var coders = []; + value.forEach(function(value) { coders.push(coder); }); + + return utils.concat([result, pack(coders, value)]); + }, + decode: function(data, offset) { + // @TODO: + //if (data.length < offset + length * 32) { throw new Error('invalid array'); } + + var consumed = 0; + + var count = length; + + if (count === -1) { + try { + var decodedLength = uint256Coder.decode(data, offset); + } catch (error) { + errors.throwError('insufficient data for dynamic array length', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'array', + value: error.value + }); + } + try { + count = decodedLength.value.toNumber(); + } catch (error) { + errors.throwError('array count too large', errors.INVALID_ARGUMENT, { + arg: localName, + coderType: 'array', + value: decodedLength.value.toString() + }); + } + consumed += decodedLength.consumed; + offset += decodedLength.consumed; + } + + // We don't want the children to have a localName + var subCoder = { + name: coder.name, + type: coder.type, + encode: coder.encode, + decode: coder.decode, + dynamic: coder.dynamic + }; + + var coders = []; + for (var i = 0; i < count; i++) { coders.push(subCoder); } + + var result = unpack(coders, data, offset); + result.consumed += consumed; + result.value = coerceFunc(type, result.value); + return result; + }, + dynamic: (length === -1 || coder.dynamic) + } +} + + +function coderTuple(coerceFunc, coders, localName) { + + var dynamic = false; + var types = []; + coders.forEach(function(coder) { + if (coder.dynamic) { dynamic = true; } + types.push(coder.type); + }); + + var type = ('tuple(' + types.join(',') + ')'); + + return { + coders: coders, + localName: localName, + name: 'tuple', + type: type, + encode: function(value) { + return pack(coders, value); + }, + decode: function(data, offset) { + var result = unpack(coders, data, offset); + result.value = coerceFunc(type, result.value); + return result; + }, + dynamic: dynamic + }; +} +/* +function getTypes(coders) { + var type = coderTuple(coders).type; + return type.substring(6, type.length - 1); +} +*/ +function splitNesting(value) { + var result = []; + var accum = ''; + var depth = 0; + for (var offset = 0; offset < value.length; offset++) { + var c = value[offset]; + if (c === ',' && depth === 0) { + result.push(accum); + accum = ''; + } else { + accum += c; + if (c === '(') { + depth++; + } else if (c === ')') { + depth--; + if (depth === -1) { + throw new Error('unbalanced parenthsis'); + } + } + } + } + result.push(accum); + + return result; +} + +var paramTypeSimple = { + address: coderAddress, + bool: coderBoolean, + string: coderString, + bytes: coderDynamicBytes, +}; + +function getTupleParamCoder(coerceFunc, components, localName) { + if (!components) { components = []; } + var coders = []; + components.forEach(function(component) { + coders.push(getParamCoder(coerceFunc, component)); + }); + + return coderTuple(coerceFunc, coders, localName); +} + +function getParamCoder(coerceFunc, param) { + var coder = paramTypeSimple[param.type]; + if (coder) { return coder(coerceFunc, param.name); } + + var match = param.type.match(paramTypeNumber); + if (match) { + var size = parseInt(match[2] || 256); + if (size === 0 || size > 256 || (size % 8) !== 0) { + errors.throwError('invalid ' + match[1] + ' bit length', errors.INVALID_ARGUMENT, { + arg: 'param', + value: param + }); + } + return coderNumber(coerceFunc, size / 8, (match[1] === 'int'), param.name); + } + + var match = param.type.match(paramTypeBytes); + if (match) { + var size = parseInt(match[1]); + if (size === 0 || size > 32) { + errors.throwError('invalid bytes length', errors.INVALID_ARGUMENT, { + arg: 'param', + value: param + }); + } + return coderFixedBytes(coerceFunc, size, param.name); + } + + var match = param.type.match(paramTypeArray); + if (match) { + param = shallowCopy(param); + var size = parseInt(match[2] || -1); + param.type = match[1]; + return coderArray(coerceFunc, getParamCoder(coerceFunc, param), size, param.name); + } + + if (param.type.substring(0, 5) === 'tuple') { + return getTupleParamCoder(coerceFunc, param.components, param.name); + } + + if (type === '') { + return coderNull(coerceFunc); + } + + errors.throwError('invalid type', errors.INVALID_ARGUMENT, { + arg: 'type', + value: type + }); +} + +function Coder(coerceFunc) { + if (!(this instanceof Coder)) { throw new Error('missing new'); } + if (!coerceFunc) { coerceFunc = defaultCoerceFunc; } + utils.defineProperty(this, 'coerceFunc', coerceFunc); +} + +// Legacy name support +// @TODO: In the next major version, remove names from decode/encode and don't do this +function populateNames(type, name) { + if (!name) { return; } + + if (type.type.substring(0, 5) === 'tuple' && typeof(name) !== 'string') { + if (type.components.length != name.names.length) { + errors.throwError('names/types length mismatch', errors.INVALID_ARGUMENT, { + count: { names: name.names.length, types: type.components.length }, + value: { names: name.names, types: type.components } + }); + } + + name.names.forEach(function(name, index) { + populateNames(type.components[index], name); + }); + + name = (name.name || ''); + } + + if (!type.name && typeof(name) === 'string') { + type.name = name; + } +} + +utils.defineProperty(Coder.prototype, 'encode', function(names, types, values) { + + // Names is optional, so shift over all the parameters if not provided + if (arguments.length < 3) { + values = types; + types = names; + names = []; + } + + if (types.length !== values.length) { + errors.throwError('types/values length mismatch', errors.INVALID_ARGUMENT, { + count: { types: types.length, values: values.length }, + value: { types: types, values: values } + }); + } + + var coders = []; + types.forEach(function(type, index) { + // Convert types to type objects + // - "uint foo" => { type: "uint", name: "foo" } + // - "tuple(uint, uint)" => { type: "tuple", components: [ { type: "uint" }, { type: "uint" }, ] } + if (typeof(type) === 'string') { + type = parseParam(type); + } + + // Legacy support for passing in names (this is going away in the next major version) + populateNames(type, names[index]); + + coders.push(getParamCoder(this.coerceFunc, type)); + }, this); + + return utils.hexlify(coderTuple(this.coerceFunc, coders).encode(values)); +}); + +utils.defineProperty(Coder.prototype, 'decode', function(names, types, data) { + + // Names is optional, so shift over all the parameters if not provided + if (arguments.length < 3) { + data = types; + types = names; + names = []; + } + + data = utils.arrayify(data); + + var coders = []; + types.forEach(function(type, index) { + + // See encode for details + if (typeof(type) === 'string') { + type = parseParam(type); + } + + // Legacy; going away in the next major version + populateNames(type, names[index]); + + coders.push(getParamCoder(this.coerceFunc, type)); + }, this); + + return coderTuple(this.coerceFunc, coders).decode(data, 0).value; + +}); + +utils.defineProperty(Coder, 'defaultCoder', new Coder()); + +utils.defineProperty(Coder, 'parseSignature', parseSignature); + + +module.exports = Coder diff --git a/utils/address.js b/utils/address.js new file mode 100644 index 0000000..dfbb9dd --- /dev/null +++ b/utils/address.js @@ -0,0 +1,124 @@ + +var BN = require('bn.js'); + +var convert = require('./convert'); +var throwError = require('./throw-error'); +var keccak256 = require('./keccak256'); + +function getChecksumAddress(address) { + if (typeof(address) !== 'string' || !address.match(/^0x[0-9A-Fa-f]{40}$/)) { + throwError('invalid address', {input: address}); + } + + address = address.toLowerCase(); + + var hashed = address.substring(2).split(''); + for (var i = 0; i < hashed.length; i++) { + hashed[i] = hashed[i].charCodeAt(0); + } + hashed = convert.arrayify(keccak256(hashed)); + + address = address.substring(2).split(''); + for (var i = 0; i < 40; i += 2) { + if ((hashed[i >> 1] >> 4) >= 8) { + address[i] = address[i].toUpperCase(); + } + if ((hashed[i >> 1] & 0x0f) >= 8) { + address[i + 1] = address[i + 1].toUpperCase(); + } + } + + return '0x' + address.join(''); +} + +// Shims for environments that are missing some required constants and functions +var MAX_SAFE_INTEGER = 0x1fffffffffffff; + +function log10(x) { + if (Math.log10) { return Math.log10(x); } + return Math.log(x) / Math.LN10; +} + + +// See: https://en.wikipedia.org/wiki/International_Bank_Account_Number +var ibanChecksum = (function() { + + // Create lookup table + var ibanLookup = {}; + for (var i = 0; i < 10; i++) { ibanLookup[String(i)] = String(i); } + for (var i = 0; i < 26; i++) { ibanLookup[String.fromCharCode(65 + i)] = String(10 + i); } + + // How many decimal digits can we process? (for 64-bit float, this is 15) + var safeDigits = Math.floor(log10(MAX_SAFE_INTEGER)); + + return function(address) { + address = address.toUpperCase(); + address = address.substring(4) + address.substring(0, 2) + '00'; + + var expanded = address.split(''); + for (var i = 0; i < expanded.length; i++) { + expanded[i] = ibanLookup[expanded[i]]; + } + expanded = expanded.join(''); + + // Javascript can handle integers safely up to 15 (decimal) digits + while (expanded.length >= safeDigits){ + var block = expanded.substring(0, safeDigits); + expanded = parseInt(block, 10) % 97 + expanded.substring(block.length); + } + + var checksum = String(98 - (parseInt(expanded, 10) % 97)); + while (checksum.length < 2) { checksum = '0' + checksum; } + + return checksum; + }; +})(); + +function getAddress(address, icapFormat) { + var result = null; + + if (typeof(address) !== 'string') { + throwError('invalid address', {input: address}); + } + + if (address.match(/^(0x)?[0-9a-fA-F]{40}$/)) { + + // Missing the 0x prefix + if (address.substring(0, 2) !== '0x') { address = '0x' + address; } + + result = getChecksumAddress(address); + + // It is a checksummed address with a bad checksum + if (address.match(/([A-F].*[a-f])|([a-f].*[A-F])/) && result !== address) { + throwError('invalid address checksum', { input: address, expected: result }); + } + + // Maybe ICAP? (we only support direct mode) + } else if (address.match(/^XE[0-9]{2}[0-9A-Za-z]{30,31}$/)) { + + // It is an ICAP address with a bad checksum + if (address.substring(2, 4) !== ibanChecksum(address)) { + throwError('invalid address icap checksum', { input: address }); + } + + result = (new BN(address.substring(4), 36)).toString(16); + while (result.length < 40) { result = '0' + result; } + result = getChecksumAddress('0x' + result); + + } else { + throwError('invalid address', { input: address }); + } + + if (icapFormat) { + var base36 = (new BN(result.substring(2), 16)).toString(36).toUpperCase(); + while (base36.length < 30) { base36 = '0' + base36; } + return 'XE' + ibanChecksum('XE00' + base36) + base36; + } + + return result; +} + + +module.exports = { + getAddress: getAddress, +} diff --git a/utils/base64.js b/utils/base64.js new file mode 100644 index 0000000..6f9381a --- /dev/null +++ b/utils/base64.js @@ -0,0 +1,13 @@ +'use strict'; + +var convert = require('./convert'); + +module.exports = { + decode: function(textData) { + return convert.arrayify(new Buffer(textData, 'base64')); + }, + + encode: function(data) { + return (new Buffer(convert.arrayify(data))).toString('base64'); + } +}; diff --git a/utils/bignumber.js b/utils/bignumber.js new file mode 100644 index 0000000..efc96da --- /dev/null +++ b/utils/bignumber.js @@ -0,0 +1,149 @@ +/** + * BigNumber + * + * A wrapper around the BN.js object. In the future we can swap out + * the underlying BN.js library for something smaller. + */ + +var BN = require('bn.js'); + +var defineProperty = require('./properties').defineProperty; +var convert = require('./convert'); +var throwError = require('./throw-error'); + +function BigNumber(value) { + if (!(this instanceof BigNumber)) { throw new Error('missing new'); } + + if (convert.isHexString(value)) { + if (value == '0x') { value = '0x0'; } + value = new BN(value.substring(2), 16); + } else if (typeof(value) === 'string' && value[0] === '-' && convert.isHexString(value.substring(1))) { + value = (new BN(value.substring(3), 16)).mul(BigNumber.constantNegativeOne._bn); + + } else if (typeof(value) === 'string' && value.match(/^-?[0-9]*$/)) { + if (value == '') { value = '0'; } + value = new BN(value); + + } else if (typeof(value) === 'number' && parseInt(value) == value) { + value = new BN(value); + + } else if (BN.isBN(value)) { + //value = value + + } else if (isBigNumber(value)) { + value = value._bn; + + } else if (convert.isArrayish(value)) { + value = new BN(convert.hexlify(value).substring(2), 16); + + } else { + throwError('invalid BigNumber value', { input: value }); + } + + defineProperty(this, '_bn', value); +} + +defineProperty(BigNumber, 'constantNegativeOne', bigNumberify(-1)); +defineProperty(BigNumber, 'constantZero', bigNumberify(0)); +defineProperty(BigNumber, 'constantOne', bigNumberify(1)); +defineProperty(BigNumber, 'constantTwo', bigNumberify(2)); +defineProperty(BigNumber, 'constantWeiPerEther', bigNumberify(new BN('1000000000000000000'))); + + +defineProperty(BigNumber.prototype, 'fromTwos', function(value) { + return new BigNumber(this._bn.fromTwos(value)); +}); + +defineProperty(BigNumber.prototype, 'toTwos', function(value) { + return new BigNumber(this._bn.toTwos(value)); +}); + + +defineProperty(BigNumber.prototype, 'add', function(other) { + return new BigNumber(this._bn.add(bigNumberify(other)._bn)); +}); + +defineProperty(BigNumber.prototype, 'sub', function(other) { + return new BigNumber(this._bn.sub(bigNumberify(other)._bn)); +}); + + +defineProperty(BigNumber.prototype, 'div', function(other) { + return new BigNumber(this._bn.div(bigNumberify(other)._bn)); +}); + +defineProperty(BigNumber.prototype, 'mul', function(other) { + return new BigNumber(this._bn.mul(bigNumberify(other)._bn)); +}); + +defineProperty(BigNumber.prototype, 'mod', function(other) { + return new BigNumber(this._bn.mod(bigNumberify(other)._bn)); +}); + +defineProperty(BigNumber.prototype, 'pow', function(other) { + return new BigNumber(this._bn.pow(bigNumberify(other)._bn)); +}); + + +defineProperty(BigNumber.prototype, 'maskn', function(value) { + return new BigNumber(this._bn.maskn(value)); +}); + + + +defineProperty(BigNumber.prototype, 'eq', function(other) { + return this._bn.eq(bigNumberify(other)._bn); +}); + +defineProperty(BigNumber.prototype, 'lt', function(other) { + return this._bn.lt(bigNumberify(other)._bn); +}); + +defineProperty(BigNumber.prototype, 'lte', function(other) { + return this._bn.lte(bigNumberify(other)._bn); +}); + +defineProperty(BigNumber.prototype, 'gt', function(other) { + return this._bn.gt(bigNumberify(other)._bn); +}); + +defineProperty(BigNumber.prototype, 'gte', function(other) { + return this._bn.gte(bigNumberify(other)._bn); +}); + + +defineProperty(BigNumber.prototype, 'isZero', function() { + return this._bn.isZero(); +}); + + +defineProperty(BigNumber.prototype, 'toNumber', function(base) { + return this._bn.toNumber(); +}); + +defineProperty(BigNumber.prototype, 'toString', function() { + //return this._bn.toString(base || 10); + return this._bn.toString(10); +}); + +defineProperty(BigNumber.prototype, 'toHexString', function() { + var hex = this._bn.toString(16); + if (hex.length % 2) { hex = '0' + hex; } + return '0x' + hex; +}); + + +function isBigNumber(value) { + return (value._bn && value._bn.mod); +} + +function bigNumberify(value) { + if (isBigNumber(value)) { return value; } + return new BigNumber(value); +} + +module.exports = { + isBigNumber: isBigNumber, + bigNumberify: bigNumberify, + BigNumber: BigNumber +}; diff --git a/utils/browser-base64.js b/utils/browser-base64.js new file mode 100644 index 0000000..319a83c --- /dev/null +++ b/utils/browser-base64.js @@ -0,0 +1,24 @@ +'use strict'; + +var convert = require('./convert'); + +module.exports = { + decode: function(textData) { + textData = atob(textData); + var data = []; + for (var i = 0; i < textData.length; i++) { + data.push(textData.charCodeAt(i)); + } + return convert.arrayify(data); + }, + encode: function(data) { + data = convert.arrayify(data); + var textData = ''; + for (var i = 0; i < data.length; i++) { + textData += String.fromCharCode(data[i]); + } + return btoa(textData); + } +}; + + diff --git a/utils/browser-random-bytes.js b/utils/browser-random-bytes.js new file mode 100644 index 0000000..49a2da5 --- /dev/null +++ b/utils/browser-random-bytes.js @@ -0,0 +1,43 @@ +'use strict'; + +var convert = require('./convert'); +var defineProperty = require('./properties').defineProperty; + +var crypto = global.crypto || global.msCrypto; +if (!crypto || !crypto.getRandomValues) { + + console.log('WARNING: Missing strong random number source; using weak randomBytes'); + + crypto = { + getRandomValues: function(buffer) { + for (var round = 0; round < 20; round++) { + for (var i = 0; i < buffer.length; i++) { + if (round) { + buffer[i] ^= parseInt(256 * Math.random()); + } else { + buffer[i] = parseInt(256 * Math.random()); + } + } + } + + return buffer; + }, + _weakCrypto: true + }; +} + +function randomBytes(length) { + if (length <= 0 || length > 1024 || parseInt(length) != length) { + throw new Error('invalid length'); + } + + var result = new Uint8Array(length); + crypto.getRandomValues(result); + return convert.arrayify(result); +}; + +if (crypto._weakCrypto === true) { + defineProperty(randomBytes, '_weakCrypto', true); +} + +module.exports = randomBytes; diff --git a/utils/contract-address.js b/utils/contract-address.js new file mode 100644 index 0000000..61a7724 --- /dev/null +++ b/utils/contract-address.js @@ -0,0 +1,20 @@ + +var getAddress = require('./address').getAddress; +var convert = require('./convert'); +var keccak256 = require('./keccak256'); +var RLP = require('./rlp'); + +// http://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed +function getContractAddress(transaction) { + if (!transaction.from) { throw new Error('missing from address'); } + var nonce = transaction.nonce; + + return getAddress('0x' + keccak256(RLP.encode([ + getAddress(transaction.from), + convert.stripZeros(convert.hexlify(nonce, 'nonce')) + ])).substring(26)); +} + +module.exports = { + getContractAddress: getContractAddress, +} diff --git a/utils/convert.js b/utils/convert.js new file mode 100644 index 0000000..9e957d5 --- /dev/null +++ b/utils/convert.js @@ -0,0 +1,224 @@ +/** + * Conversion Utilities + * + */ + +var defineProperty = require('./properties.js').defineProperty; + +var errors = require('./errors'); + +function addSlice(array) { + if (array.slice) { return array; } + + array.slice = function() { + var args = Array.prototype.slice.call(arguments); + return new Uint8Array(Array.prototype.slice.apply(array, args)); + } + + return array; +} + +function isArrayish(value) { + if (!value || parseInt(value.length) != value.length || typeof(value) === 'string') { + return false; + } + + for (var i = 0; i < value.length; i++) { + var v = value[i]; + if (v < 0 || v >= 256 || parseInt(v) != v) { + return false; + } + } + + return true; +} + +function arrayify(value) { + if (value == null) { + errors.throwError('cannot convert null value to array', errors.INVALID_ARGUMENT, { arg: 'value', value: value }); + } + + if (value && value.toHexString) { + value = value.toHexString(); + } + + if (isHexString(value)) { + value = value.substring(2); + if (value.length % 2) { value = '0' + value; } + + var result = []; + for (var i = 0; i < value.length; i += 2) { + result.push(parseInt(value.substr(i, 2), 16)); + } + + return addSlice(new Uint8Array(result)); + + } else if (typeof(value) === 'string') { + if (value.match(/^[0-9a-fA-F]*$/)) { + errors.throwError('hex string must have 0x prefix', errors.INVALID_ARGUMENT, { arg: 'value', value: value }); + } + errors.throwError('invalid hexidecimal string', errors.INVALID_ARGUMENT, { arg: 'value', value: value }); + } + + if (isArrayish(value)) { + return addSlice(new Uint8Array(value)); + } + + errors.throwError('invalid arrayify value', { arg: 'value', value: value, type: typeof(value) }); +} + +function concat(objects) { + var arrays = []; + var length = 0; + for (var i = 0; i < objects.length; i++) { + var object = arrayify(objects[i]) + arrays.push(object); + length += object.length; + } + + var result = new Uint8Array(length); + var offset = 0; + for (var i = 0; i < arrays.length; i++) { + result.set(arrays[i], offset); + offset += arrays[i].length; + } + + return addSlice(result); +} +function stripZeros(value) { + value = arrayify(value); + + if (value.length === 0) { return value; } + + // Find the first non-zero entry + var start = 0; + while (value[start] === 0) { start++ } + + // If we started with zeros, strip them + if (start) { + value = value.slice(start); + } + + return value; +} + +function padZeros(value, length) { + value = arrayify(value); + + if (length < value.length) { throw new Error('cannot pad'); } + + var result = new Uint8Array(length); + result.set(value, length - value.length); + return addSlice(result); +} + + +function isHexString(value, length) { + if (typeof(value) !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) { + return false + } + if (length && value.length !== 2 + 2 * length) { return false; } + return true; +} + +var HexCharacters = '0123456789abcdef'; + +function hexlify(value) { + + if (value && value.toHexString) { + return value.toHexString(); + } + + if (typeof(value) === 'number') { + if (value < 0) { + errors.throwError('cannot hexlify negative value', errors.INVALID_ARG, { arg: 'value', value: value }); + } + + var hex = ''; + while (value) { + hex = HexCharacters[value & 0x0f] + hex; + value = parseInt(value / 16); + } + + if (hex.length) { + if (hex.length % 2) { hex = '0' + hex; } + return '0x' + hex; + } + + return '0x00'; + } + + if (isHexString(value)) { + if (value.length % 2) { + value = '0x0' + value.substring(2); + } + return value; + } + + if (isArrayish(value)) { + var result = []; + for (var i = 0; i < value.length; i++) { + var v = value[i]; + result.push(HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f]); + } + return '0x' + result.join(''); + } + + errors.throwError('invalid hexlify value', { arg: 'value', value: value }); +} + +function hexStripZeros(value) { + while (value.length > 3 && value.substring(0, 3) === '0x0') { + value = '0x' + value.substring(3); + } + return value; +} + +function hexZeroPad(value, length) { + while (value.length < 2 * length + 2) { + value = '0x0' + value.substring(2); + } + return value; +} + +/* @TODO: Add something like this to make slicing code easier to understand +function hexSlice(hex, start, end) { + hex = hexlify(hex); + return '0x' + hex.substring(2 + start * 2, 2 + end * 2); +} +*/ + +function splitSignature(signature) { + signature = arrayify(signature); + if (signature.length !== 65) { + throw new Error('invalid signature'); + } + + var v = signature[64]; + if (v !== 27 && v !== 28) { + v = 27 + (v % 2); + } + + return { + r: hexlify(signature.slice(0, 32)), + s: hexlify(signature.slice(32, 64)), + v: v + } +} + +module.exports = { + arrayify: arrayify, + isArrayish: isArrayish, + + concat: concat, + + padZeros: padZeros, + stripZeros: stripZeros, + + splitSignature: splitSignature, + + hexlify: hexlify, + isHexString: isHexString, + hexStripZeros: hexStripZeros, + hexZeroPad: hexZeroPad, +}; diff --git a/utils/empty.js b/utils/empty.js new file mode 100644 index 0000000..d0f5f3a --- /dev/null +++ b/utils/empty.js @@ -0,0 +1 @@ +module.exports = undefined; diff --git a/utils/errors.js b/utils/errors.js new file mode 100644 index 0000000..00c5a4d --- /dev/null +++ b/utils/errors.js @@ -0,0 +1,91 @@ +'use strict'; + +var defineProperty = require('./properties').defineProperty; + +var codes = { }; + +[ + // Unknown Error + 'UNKNOWN_ERROR', + + // Not implemented + 'NOT_IMPLEMENTED', + + // Missing new operator to an object + // - name: The name of the class + 'MISSING_NEW', + + + // Call exception + 'CALL_EXCEPTION', + + + // Response from a server was invalid + // - response: The body of the response + //'BAD_RESPONSE', + + + // Invalid argument (e.g. type) to a function: + // - arg: The argument name that was invalid + // - value: The value of the argument + // - type: The type of the argument + // - expected: What was expected + 'INVALID_ARGUMENT', + + // Missing argument to a function: + // - arg: The argument name that is required + // - count: The number of arguments received + // - expectedCount: The number of arguments expected + 'MISSING_ARGUMENT', + + // Too many arguments + // - count: The number of arguments received + // - expectedCount: The number of arguments expected + 'UNEXPECTED_ARGUMENT', + + + // Unsupported operation + // - operation + 'UNSUPPORTED_OPERATION', + + +].forEach(function(code) { + defineProperty(codes, code, code); +}); + + +defineProperty(codes, 'throwError', function(message, code, params) { + if (!code) { code = codes.UNKNOWN_ERROR; } + if (!params) { params = {}; } + + var messageDetails = []; + Object.keys(params).forEach(function(key) { + try { + messageDetails.push(key + '=' + JSON.stringify(params[key])); + } catch (error) { + messageDetails.push(key + '=' + JSON.stringify(params[key].toString())); + } + }); + var reason = message; + if (messageDetails.length) { + message += ' (' + messageDetails.join(', ') + ')'; + } + + var error = new Error(message); + error.reason = reason; + error.code = code + + Object.keys(params).forEach(function(key) { + error[key] = params[key]; + }); + + throw error; +}); + +defineProperty(codes, 'checkNew', function(self, kind) { + if (!(self instanceof kind)) { + codes.throwError('missing new', codes.MISSING_NEW, { name: kind.name }); + } +}); + +module.exports = codes; diff --git a/utils/hdnode.js b/utils/hdnode.js new file mode 100644 index 0000000..145ef98 --- /dev/null +++ b/utils/hdnode.js @@ -0,0 +1,259 @@ +// See: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +// See: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki + +var secp256k1 = new (require('elliptic')).ec('secp256k1'); + +var wordlist = (function() { + var words = require('./words.json'); + return words.replace(/([A-Z])/g, ' $1').toLowerCase().substring(1).split(' '); +})(); + +var utils = (function() { + var convert = require('./convert.js'); + + var sha2 = require('./sha2'); + + var hmac = require('./hmac'); + + return { + defineProperty: require('./properties.js').defineProperty, + + arrayify: convert.arrayify, + bigNumberify: require('./bignumber.js').bigNumberify, + hexlify: convert.hexlify, + + toUtf8Bytes: require('./utf8.js').toUtf8Bytes, + + sha256: sha2.sha256, + createSha512Hmac: hmac.createSha512Hmac, + + pbkdf2: require('./pbkdf2.js'), + } +})(); + +// "Bitcoin seed" +var MasterSecret = utils.toUtf8Bytes('Bitcoin seed'); + +var HardenedBit = 0x80000000; + +// Returns a byte with the MSB bits set +function getUpperMask(bits) { + return ((1 << bits) - 1) << (8 - bits); +} + +// Returns a byte with the LSB bits set +function getLowerMask(bits) { + return (1 << bits) - 1; +} + +function HDNode(keyPair, chainCode, index, depth) { + if (!(this instanceof HDNode)) { throw new Error('missing new'); } + + utils.defineProperty(this, '_keyPair', keyPair); + + utils.defineProperty(this, 'privateKey', utils.hexlify(keyPair.priv.toArray('be', 32))); + utils.defineProperty(this, 'publicKey', '0x' + keyPair.getPublic(true, 'hex')); + + utils.defineProperty(this, 'chainCode', utils.hexlify(chainCode)); + + utils.defineProperty(this, 'index', index); + utils.defineProperty(this, 'depth', depth); +} + +utils.defineProperty(HDNode.prototype, '_derive', function(index) { + + // Public parent key -> public child key + if (!this.privateKey) { + if (index >= HardenedBit) { throw new Error('cannot derive child of neutered node'); } + throw new Error('not implemented'); + } + + var data = new Uint8Array(37); + + if (index & HardenedBit) { + // Data = 0x00 || ser_256(k_par) + data.set(utils.arrayify(this.privateKey), 1); + + } else { + // Data = ser_p(point(k_par)) + data.set(this._keyPair.getPublic().encode(null, true)); + } + + // Data += ser_32(i) + for (var i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff); } + + var I = utils.arrayify(utils.createSha512Hmac(this.chainCode).update(data).digest()); + var IL = utils.bigNumberify(I.slice(0, 32)); + var IR = I.slice(32); + + var ki = IL.add('0x' + this._keyPair.getPrivate('hex')).mod('0x' + secp256k1.curve.n.toString(16)); + + return new HDNode(secp256k1.keyFromPrivate(utils.arrayify(ki)), I.slice(32), index, this.depth + 1); +}); + +utils.defineProperty(HDNode.prototype, 'derivePath', function(path) { + var components = path.split('/'); + + if (components.length === 0 || (components[0] === 'm' && this.depth !== 0)) { + throw new Error('invalid path'); + } + + if (components[0] === 'm') { components.shift(); } + + var result = this; + for (var i = 0; i < components.length; i++) { + var component = components[i]; + if (component.match(/^[0-9]+'$/)) { + var index = parseInt(component.substring(0, component.length - 1)); + if (index >= HardenedBit) { throw new Error('invalid path index - ' + component); } + result = result._derive(HardenedBit + index); + } else if (component.match(/^[0-9]+$/)) { + var index = parseInt(component); + if (index >= HardenedBit) { throw new Error('invalid path index - ' + component); } + result = result._derive(index); + } else { + throw new Error('invlaid path component - ' + component); + } + } + + return result; +}); + +utils.defineProperty(HDNode, 'fromMnemonic', function(mnemonic) { + // Check that the checksum s valid (will throw an error) + mnemonicToEntropy(mnemonic); + + return HDNode.fromSeed(mnemonicToSeed(mnemonic)); +}); + +utils.defineProperty(HDNode, 'fromSeed', function(seed) { + seed = utils.arrayify(seed); + if (seed.length < 16 || seed.length > 64) { throw new Error('invalid seed'); } + + var I = utils.arrayify(utils.createSha512Hmac(MasterSecret).update(seed).digest()); + + return new HDNode(secp256k1.keyFromPrivate(I.slice(0, 32)), I.slice(32), 0, 0, 0); +}); + +function mnemonicToSeed(mnemonic, password) { + + if (!password) { + password = ''; + + } else if (password.normalize) { + password = password.normalize('NFKD'); + + } else { + for (var i = 0; i < password.length; i++) { + var c = password.charCodeAt(i); + if (c < 32 || c > 127) { throw new Error('passwords with non-ASCII characters not supported in this environment'); } + } + } + + mnemonic = utils.toUtf8Bytes(mnemonic, 'NFKD'); + var salt = utils.toUtf8Bytes('mnemonic' + password, 'NFKD'); + + return utils.hexlify(utils.pbkdf2(mnemonic, salt, 2048, 64, utils.createSha512Hmac)); +} + +function mnemonicToEntropy(mnemonic) { + var words = mnemonic.toLowerCase().split(' '); + if ((words.length % 3) !== 0) { throw new Error('invalid mnemonic'); } + + var entropy = utils.arrayify(new Uint8Array(Math.ceil(11 * words.length / 8))); + + var offset = 0; + for (var i = 0; i < words.length; i++) { + var index = wordlist.indexOf(words[i]); + if (index === -1) { throw new Error('invalid mnemonic'); } + + for (var bit = 0; bit < 11; bit++) { + if (index & (1 << (10 - bit))) { + entropy[offset >> 3] |= (1 << (7 - (offset % 8))); + } + offset++; + } + } + + var entropyBits = 32 * words.length / 3; + + var checksumBits = words.length / 3; + var checksumMask = getUpperMask(checksumBits); + + var checksum = utils.arrayify(utils.sha256(entropy.slice(0, entropyBits / 8)))[0]; + checksum &= checksumMask; + + if (checksum !== (entropy[entropy.length - 1] & checksumMask)) { + throw new Error('invalid checksum'); + } + + return utils.hexlify(entropy.slice(0, entropyBits / 8)); +} + +function entropyToMnemonic(entropy) { + entropy = utils.arrayify(entropy); + + if ((entropy.length % 4) !== 0 || entropy.length < 16 || entropy.length > 32) { + throw new Error('invalid entropy'); + } + + var words = [0]; + + var remainingBits = 11; + for (var i = 0; i < entropy.length; i++) { + + // Consume the whole byte (with still more to go) + if (remainingBits > 8) { + words[words.length - 1] <<= 8; + words[words.length - 1] |= entropy[i]; + + remainingBits -= 8; + + // This byte will complete an 11-bit index + } else { + words[words.length - 1] <<= remainingBits; + words[words.length - 1] |= entropy[i] >> (8 - remainingBits); + + // Start the next word + words.push(entropy[i] & getLowerMask(8 - remainingBits)); + + remainingBits += 3; + } + } + + // Compute the checksum bits + var checksum = utils.arrayify(utils.sha256(entropy))[0]; + var checksumBits = entropy.length / 4; + checksum &= getUpperMask(checksumBits); + + // Shift the checksum into the word indices + words[words.length - 1] <<= checksumBits; + words[words.length - 1] |= (checksum >> (8 - checksumBits)); + + // Convert indices into words + for (var i = 0; i < words.length; i++) { + words[i] = wordlist[words[i]]; + } + + return words.join(' '); +} + +function isValidMnemonic(mnemonic) { + try { + mnemonicToEntropy(mnemonic); + return true; + } catch (error) { } + + return false; +} + +module.exports = { + fromMnemonic: HDNode.fromMnemonic, + fromSeed: HDNode.fromSeed, + + mnemonicToSeed: mnemonicToSeed, + mnemonicToEntropy: mnemonicToEntropy, + entropyToMnemonic: entropyToMnemonic, + + isValidMnemonic: isValidMnemonic, +}; diff --git a/utils/hmac.js b/utils/hmac.js new file mode 100644 index 0000000..a789904 --- /dev/null +++ b/utils/hmac.js @@ -0,0 +1,24 @@ +'use strict'; + +var hash = require('hash.js'); + +var sha2 = require('./sha2.js'); + +var convert = require('./convert.js'); + +// @TODO: Make this use create-hmac in node + +function createSha256Hmac(key) { + if (!key.buffer) { key = convert.arrayify(key); } + return new hash.hmac(sha2.createSha256, key); +} + +function createSha512Hmac(key) { + if (!key.buffer) { key = convert.arrayify(key); } + return new hash.hmac(sha2.createSha512, key); +} + +module.exports = { + createSha256Hmac: createSha256Hmac, + createSha512Hmac: createSha512Hmac, +}; diff --git a/utils/id.js b/utils/id.js new file mode 100644 index 0000000..b0723e0 --- /dev/null +++ b/utils/id.js @@ -0,0 +1,10 @@ +'use strict'; + +var keccak256 = require('./keccak256'); +var utf8 = require('./utf8'); + +function id(text) { + return keccak256(utf8.toUtf8Bytes(text)); +} + +module.exports = id; diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..7ecd626 --- /dev/null +++ b/utils/index.js @@ -0,0 +1,75 @@ +'use strict'; + +// This is SUPER useful, but adds 140kb (even zipped, adds 40kb) +//var unorm = require('unorm'); + +var address = require('./address'); +var AbiCoder = require('./abi-coder'); +var base64 = require('./base64'); +var bigNumber = require('./bignumber'); +var contractAddress = require('./contract-address'); +var convert = require('./convert'); +var id = require('./id'); +var keccak256 = require('./keccak256'); +var namehash = require('./namehash'); +var sha256 = require('./sha2').sha256; +var solidity = require('./solidity'); +var randomBytes = require('./random-bytes'); +var properties = require('./properties'); +var RLP = require('./rlp'); +var utf8 = require('./utf8'); +var units = require('./units'); + + +module.exports = { + AbiCoder: AbiCoder, + + RLP: RLP, + + defineProperty: properties.defineProperty, + + // NFKD (decomposed) + //etherSymbol: '\uD835\uDF63', + + // NFKC (composed) + etherSymbol: '\u039e', + + arrayify: convert.arrayify, + + concat: convert.concat, + padZeros: convert.padZeros, + stripZeros: convert.stripZeros, + + base64: base64, + + bigNumberify: bigNumber.bigNumberify, + BigNumber: bigNumber.BigNumber, + + hexlify: convert.hexlify, + + toUtf8Bytes: utf8.toUtf8Bytes, + toUtf8String: utf8.toUtf8String, + + namehash: namehash, + id: id, + + getAddress: address.getAddress, + getContractAddress: contractAddress.getContractAddress, + + formatEther: units.formatEther, + parseEther: units.parseEther, + + formatUnits: units.formatUnits, + parseUnits: units.parseUnits, + + keccak256: keccak256, + sha256: sha256, + + randomBytes: randomBytes, + + solidityPack: solidity.pack, + solidityKeccak256: solidity.keccak256, + soliditySha256: solidity.sha256, + + splitSignature: convert.splitSignature, +} diff --git a/utils/keccak256.js b/utils/keccak256.js new file mode 100644 index 0000000..0d143fb --- /dev/null +++ b/utils/keccak256.js @@ -0,0 +1,12 @@ +'use strict'; + +var sha3 = require('js-sha3'); + +var convert = require('./convert.js'); + +function keccak256(data) { + data = convert.arrayify(data); + return '0x' + sha3.keccak_256(data); +} + +module.exports = keccak256; diff --git a/utils/namehash.js b/utils/namehash.js new file mode 100644 index 0000000..b59bc1e --- /dev/null +++ b/utils/namehash.js @@ -0,0 +1,38 @@ +'use strict'; + +var convert = require('./convert'); +var utf8 = require('./utf8'); +var keccak256 = require('./keccak256'); + +var Zeros = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +var Partition = new RegExp("^((.*)\\.)?([^.]+)$"); +var UseSTD3ASCIIRules = new RegExp("^[a-z0-9.-]*$"); + +function namehash(name, depth) { + name = name.toLowerCase(); + + // Supporting the full UTF-8 space requires additional (and large) + // libraries, so for now we simply do not support them. + // It should be fairly easy in the future to support systems with + // String.normalize, but that is future work. + if (!name.match(UseSTD3ASCIIRules)) { + throw new Error('contains invalid UseSTD3ASCIIRules characters'); + } + + var result = Zeros; + var processed = 0; + while (name.length && (!depth || processed < depth)) { + var partition = name.match(Partition); + var label = utf8.toUtf8Bytes(partition[3]); + result = keccak256(convert.concat([result, keccak256(label)])); + + name = partition[2] || ''; + + processed++; + } + + return convert.hexlify(result); +} + +module.exports = namehash; + diff --git a/utils/pbkdf2.js b/utils/pbkdf2.js new file mode 100644 index 0000000..21ddc58 --- /dev/null +++ b/utils/pbkdf2.js @@ -0,0 +1,51 @@ +'use strict'; + +var convert = require('./convert'); + +function pbkdf2(password, salt, iterations, keylen, createHmac) { + var hLen + var l = 1 + var DK = new Uint8Array(keylen) + var block1 = new Uint8Array(salt.length + 4) + block1.set(salt); + //salt.copy(block1, 0, 0, salt.length) + + var r + var T + + for (var i = 1; i <= l; i++) { + //block1.writeUInt32BE(i, salt.length) + block1[salt.length] = (i >> 24) & 0xff; + block1[salt.length + 1] = (i >> 16) & 0xff; + block1[salt.length + 2] = (i >> 8) & 0xff; + block1[salt.length + 3] = i & 0xff; + + var U = createHmac(password).update(block1).digest(); + + if (!hLen) { + hLen = U.length + T = new Uint8Array(hLen) + l = Math.ceil(keylen / hLen) + r = keylen - (l - 1) * hLen + } + + //U.copy(T, 0, 0, hLen) + T.set(U); + + + for (var j = 1; j < iterations; j++) { + U = createHmac(password).update(U).digest() + for (var k = 0; k < hLen; k++) T[k] ^= U[k] + } + + + var destPos = (i - 1) * hLen + var len = (i === l ? r : hLen) + //T.copy(DK, destPos, 0, len) + DK.set(convert.arrayify(T).slice(0, len), destPos); + } + + return convert.arrayify(DK) +} + +module.exports = pbkdf2; diff --git a/utils/properties.js b/utils/properties.js new file mode 100644 index 0000000..9738ff1 --- /dev/null +++ b/utils/properties.js @@ -0,0 +1,22 @@ +'use strict'; + +function defineProperty(object, name, value) { + Object.defineProperty(object, name, { + enumerable: true, + value: value, + writable: false, + }); +} + +function defineFrozen(object, name, value) { + var frozen = JSON.stringify(value); + Object.defineProperty(object, name, { + enumerable: true, + get: function() { return JSON.parse(frozen); } + }); +} + +module.exports = { + defineFrozen: defineFrozen, + defineProperty: defineProperty, +}; diff --git a/utils/random-bytes.js b/utils/random-bytes.js new file mode 100644 index 0000000..39e334e --- /dev/null +++ b/utils/random-bytes.js @@ -0,0 +1,8 @@ +'use strict'; + +var randomBytes = require('crypto').randomBytes; + +module.exports = function(length) { + return new Uint8Array(randomBytes(length)); +} + diff --git a/utils/rlp.js b/utils/rlp.js new file mode 100644 index 0000000..4ee255d --- /dev/null +++ b/utils/rlp.js @@ -0,0 +1,142 @@ +//See: https://github.com/ethereum/wiki/wiki/RLP + +var convert = require('./convert.js'); + +function arrayifyInteger(value) { + var result = []; + while (value) { + result.unshift(value & 0xff); + value >>= 8; + } + return result; +} + +function unarrayifyInteger(data, offset, length) { + var result = 0; + for (var i = 0; i < length; i++) { + result = (result * 256) + data[offset + i]; + } + return result; +} + +function _encode(object) { + if (Array.isArray(object)) { + var payload = []; + object.forEach(function(child) { + payload = payload.concat(_encode(child)); + }); + + if (payload.length <= 55) { + payload.unshift(0xc0 + payload.length) + return payload; + } + + var length = arrayifyInteger(payload.length); + length.unshift(0xf7 + length.length); + + return length.concat(payload); + + } else { + object = [].slice.call(convert.arrayify(object)); + + if (object.length === 1 && object[0] <= 0x7f) { + return object; + + } else if (object.length <= 55) { + object.unshift(0x80 + object.length); + return object + } + + var length = arrayifyInteger(object.length); + length.unshift(0xb7 + length.length); + + return length.concat(object); + } +} + +function encode(object) { + return convert.hexlify(_encode(object)); +} + +function _decodeChildren(data, offset, childOffset, length) { + var result = []; + + while (childOffset < offset + 1 + length) { + var decoded = _decode(data, childOffset); + + result.push(decoded.result); + + childOffset += decoded.consumed; + if (childOffset > offset + 1 + length) { + throw new Error('invalid rlp'); + } + } + + return {consumed: (1 + length), result: result}; +} + +// returns { consumed: number, result: Object } +function _decode(data, offset) { + if (data.length === 0) { throw new Error('invalid rlp data'); } + + // Array with extra length prefix + if (data[offset] >= 0xf8) { + var lengthLength = data[offset] - 0xf7; + if (offset + 1 + lengthLength > data.length) { + throw new Error('too short'); + } + + var length = unarrayifyInteger(data, offset + 1, lengthLength); + if (offset + 1 + lengthLength + length > data.length) { + throw new Error('to short'); + } + + return _decodeChildren(data, offset, offset + 1 + lengthLength, lengthLength + length); + + } else if (data[offset] >= 0xc0) { + var length = data[offset] - 0xc0; + if (offset + 1 + length > data.length) { + throw new Error('invalid rlp data'); + } + + return _decodeChildren(data, offset, offset + 1, length); + + } else if (data[offset] >= 0xb8) { + var lengthLength = data[offset] - 0xb7; + if (offset + 1 + lengthLength > data.length) { + throw new Error('invalid rlp data'); + } + + var length = unarrayifyInteger(data, offset + 1, lengthLength); + if (offset + 1 + lengthLength + length > data.length) { + throw new Error('invalid rlp data'); + } + + var result = convert.hexlify(data.slice(offset + 1 + lengthLength, offset + 1 + lengthLength + length)); + return { consumed: (1 + lengthLength + length), result: result } + + } else if (data[offset] >= 0x80) { + var length = data[offset] - 0x80; + if (offset + 1 + length > data.offset) { + throw new Error('invlaid rlp data'); + } + + var result = convert.hexlify(data.slice(offset + 1, offset + 1 + length)); + return { consumed: (1 + length), result: result } + } + return { consumed: 1, result: convert.hexlify(data[offset]) }; +} + +function decode(data) { + data = convert.arrayify(data); + var decoded = _decode(data, 0); + if (decoded.consumed !== data.length) { + throw new Error('invalid rlp data'); + } + return decoded.result; +} + +module.exports = { + encode: encode, + decode: decode, +} diff --git a/utils/secret-storage.js b/utils/secret-storage.js new file mode 100644 index 0000000..066496d --- /dev/null +++ b/utils/secret-storage.js @@ -0,0 +1,449 @@ +'use strict'; + +var aes = require('aes-js'); +var scrypt = require('scrypt-js'); +var uuid = require('uuid'); + +var hmac = require('../utils/hmac'); +var pbkdf2 = require('../utils/pbkdf2'); +var utils = require('../util.js'); + +var SigningKey = require('./signing-key'); +var HDNode = require('./hdnode'); + +// @TODO: Maybe move this to HDNode? +var defaultPath = "m/44'/60'/0'/0/0"; + +function arrayify(hexString) { + if (typeof(hexString) === 'string' && hexString.substring(0, 2) !== '0x') { + hexString = '0x' + hexString; + } + return utils.arrayify(hexString); +} + +function zpad(value, length) { + value = String(value); + while (value.length < length) { value = '0' + value; } + return value; +} + +function getPassword(password) { + if (typeof(password) === 'string') { + return utils.toUtf8Bytes(password, 'NFKC'); + } + return utils.arrayify(password, 'password'); +} + +// Search an Object and its children recursively, caselessly. +function searchPath(object, path) { + var currentChild = object; + + var comps = path.toLowerCase().split('/'); + for (var i = 0; i < comps.length; i++) { + + // Search for a child object with a case-insensitive matching key + var matchingChild = null; + for (var key in currentChild) { + if (key.toLowerCase() === comps[i]) { + matchingChild = currentChild[key]; + break; + } + } + + // Didn't find one. :'( + if (matchingChild === null) { + return null; + } + + // Now check this child... + currentChild = matchingChild; + } + + return currentChild; +} + +var secretStorage = {}; + + +utils.defineProperty(secretStorage, 'isCrowdsaleWallet', function(json) { + try { + var data = JSON.parse(json); + } catch (error) { return false; } + + return (data.encseed && data.ethaddr); +}); + +utils.defineProperty(secretStorage, 'isValidWallet', function(json) { + try { + var data = JSON.parse(json); + } catch (error) { return false; } + + if (!data.version || parseInt(data.version) !== data.version || parseInt(data.version) !== 3) { + return false; + } + + // @TODO: Put more checks to make sure it has kdf, iv and all that good stuff + return true; +}); + + +// See: https://github.com/ethereum/pyethsaletool +utils.defineProperty(secretStorage, 'decryptCrowdsale', function(json, password) { + var data = JSON.parse(json); + + password = getPassword(password); + + // Ethereum Address + var ethaddr = utils.getAddress(searchPath(data, 'ethaddr')); + + // Encrypted Seed + var encseed = arrayify(searchPath(data, 'encseed')); + if (!encseed || (encseed.length % 16) !== 0) { + throw new Error('invalid encseed'); + } + + var key = pbkdf2(password, password, 2000, 32, hmac.createSha256Hmac).slice(0, 16); + + var iv = encseed.slice(0, 16); + var encryptedSeed = encseed.slice(16); + + // Decrypt the seed + var aesCbc = new aes.ModeOfOperation.cbc(key, iv); + var seed = utils.arrayify(aesCbc.decrypt(encryptedSeed)); + seed = aes.padding.pkcs7.strip(seed); + + // This wallet format is weird... Convert the binary encoded hex to a string. + var seedHex = ''; + for (var i = 0; i < seed.length; i++) { + seedHex += String.fromCharCode(seed[i]); + } + + var seedHexBytes = utils.toUtf8Bytes(seedHex); + + var signingKey = new SigningKey(utils.keccak256(seedHexBytes)); + + if (signingKey.address !== ethaddr) { + throw new Error('corrupt crowdsale wallet'); + } + + return signingKey; +}); + + +utils.defineProperty(secretStorage, 'decrypt', function(json, password, progressCallback) { + var data = JSON.parse(json); + + password = getPassword(password); + + var decrypt = function(key, ciphertext) { + var cipher = searchPath(data, 'crypto/cipher'); + if (cipher === 'aes-128-ctr') { + var iv = arrayify(searchPath(data, 'crypto/cipherparams/iv'), 'crypto/cipherparams/iv') + var counter = new aes.Counter(iv); + + var aesCtr = new aes.ModeOfOperation.ctr(key, counter); + + return arrayify(aesCtr.decrypt(ciphertext)); + } + + return null; + }; + + var computeMAC = function(derivedHalf, ciphertext) { + return utils.keccak256(utils.concat([derivedHalf, ciphertext])); + } + + var getSigningKey = function(key, reject) { + var ciphertext = arrayify(searchPath(data, 'crypto/ciphertext')); + + var computedMAC = utils.hexlify(computeMAC(key.slice(16, 32), ciphertext)).substring(2); + if (computedMAC !== searchPath(data, 'crypto/mac').toLowerCase()) { + reject(new Error('invalid password')); + return null; + } + + var privateKey = decrypt(key.slice(0, 16), ciphertext); + var mnemonicKey = key.slice(32, 64); + + if (!privateKey) { + reject(new Error('unsupported cipher')); + return null; + } + + var signingKey = new SigningKey(privateKey); + if (signingKey.address !== utils.getAddress(data.address)) { + reject(new Error('address mismatch')); + return null; + } + + // Version 0.1 x-ethers metadata must contain an encrypted mnemonic phrase + if (searchPath(data, 'x-ethers/version') === '0.1') { + var mnemonicCiphertext = arrayify(searchPath(data, 'x-ethers/mnemonicCiphertext'), 'x-ethers/mnemonicCiphertext'); + var mnemonicIv = arrayify(searchPath(data, 'x-ethers/mnemonicCounter'), 'x-ethers/mnemonicCounter'); + + var mnemonicCounter = new aes.Counter(mnemonicIv); + var mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); + + var path = searchPath(data, 'x-ethers/path') || defaultPath; + + var entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext)); + var mnemonic = HDNode.entropyToMnemonic(entropy); + + if (HDNode.fromMnemonic(mnemonic).derivePath(path).privateKey != utils.hexlify(privateKey)) { + reject(new Error('mnemonic mismatch')); + return null; + } + + signingKey.mnemonic = mnemonic; + signingKey.path = path; + } + + return signingKey; + } + + + return new Promise(function(resolve, reject) { + var kdf = searchPath(data, 'crypto/kdf'); + if (kdf && typeof(kdf) === 'string') { + if (kdf.toLowerCase() === 'scrypt') { + var salt = arrayify(searchPath(data, 'crypto/kdfparams/salt'), 'crypto/kdfparams/salt'); + var N = parseInt(searchPath(data, 'crypto/kdfparams/n')); + var r = parseInt(searchPath(data, 'crypto/kdfparams/r')); + var p = parseInt(searchPath(data, 'crypto/kdfparams/p')); + if (!N || !r || !p) { + reject(new Error('unsupported key-derivation function parameters')); + return; + } + + // Make sure N is a power of 2 + if ((N & (N - 1)) !== 0) { + reject(new Error('unsupported key-derivation function parameter value for N')); + return; + } + + var dkLen = parseInt(searchPath(data, 'crypto/kdfparams/dklen')); + if (dkLen !== 32) { + reject( new Error('unsupported key-derivation derived-key length')); + return; + } + + scrypt(password, salt, N, r, p, 64, function(error, progress, key) { + if (error) { + error.progress = progress; + reject(error); + + } else if (key) { + key = arrayify(key); + + var signingKey = getSigningKey(key, reject); + if (!signingKey) { return; } + + if (progressCallback) { progressCallback(1); } + resolve(signingKey); + + } else if (progressCallback) { + return progressCallback(progress); + } + }); + + } else if (kdf.toLowerCase() === 'pbkdf2') { + var salt = arrayify(searchPath(data, 'crypto/kdfparams/salt'), 'crypto/kdfparams/salt'); + + var prfFunc = null; + var prf = searchPath(data, 'crypto/kdfparams/prf'); + if (prf === 'hmac-sha256') { + prfFunc = hmac.createSha256Hmac; + } else if (prf === 'hmac-sha512') { + prfFunc = hmac.createSha512Hmac; + } else { + reject(new Error('unsupported prf')); + return; + } + + var c = parseInt(searchPath(data, 'crypto/kdfparams/c')); + + var dkLen = parseInt(searchPath(data, 'crypto/kdfparams/dklen')); + if (dkLen !== 32) { + reject( new Error('unsupported key-derivation derived-key length')); + return; + } + + var key = pbkdf2(password, salt, c, dkLen, prfFunc); + + var signingKey = getSigningKey(key, reject); + if (!signingKey) { return; } + + resolve(signingKey); + + } else { + reject(new Error('unsupported key-derivation function')); + } + + } else { + reject(new Error('unsupported key-derivation function')); + } + }); +}); + +utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, options, progressCallback) { + + // the options are optional, so adjust the call as needed + if (typeof(options) === 'function' && !progressCallback) { + progressCallback = options; + options = {}; + } + if (!options) { options = {}; } + + // Check the private key + if (privateKey instanceof SigningKey) { + privateKey = privateKey.privateKey; + } + privateKey = arrayify(privateKey, 'private key'); + if (privateKey.length !== 32) { throw new Error('invalid private key'); } + + password = getPassword(password); + + var entropy = options.entropy; + if (options.mnemonic) { + if (entropy) { + if (HDNode.entropyToMnemonic(entropy) !== options.mnemonic) { + throw new Error('entropy and mnemonic mismatch'); + } + } else { + entropy = HDNode.mnemonicToEntropy(options.mnemonic); + } + } + if (entropy) { + entropy = arrayify(entropy, 'entropy'); + } + + var path = options.path; + if (entropy && !path) { + path = defaultPath; + } + + var client = options.client; + if (!client) { client = "ethers.js"; } + + // Check/generate the salt + var salt = options.salt; + if (salt) { + salt = arrayify(salt, 'salt'); + } else { + salt = utils.randomBytes(32);; + } + + // Override initialization vector + var iv = null; + if (options.iv) { + iv = arrayify(options.iv, 'iv'); + if (iv.length !== 16) { throw new Error('invalid iv'); } + } else { + iv = utils.randomBytes(16); + } + + // Override the uuid + var uuidRandom = options.uuid; + if (uuidRandom) { + uuidRandom = arrayify(uuidRandom, 'uuid'); + if (uuidRandom.length !== 16) { throw new Error('invalid uuid'); } + } else { + uuidRandom = utils.randomBytes(16); + } + + // Override the scrypt password-based key derivation function parameters + var N = (1 << 17), r = 8, p = 1; + if (options.scrypt) { + if (options.scrypt.N) { N = options.scrypt.N; } + if (options.scrypt.r) { r = options.scrypt.r; } + if (options.scrypt.p) { p = options.scrypt.p; } + } + + return new Promise(function(resolve, reject) { + + // We take 64 bytes: + // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix) + // - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet) + scrypt(password, salt, N, r, p, 64, function(error, progress, key) { + if (error) { + error.progress = progress; + reject(error); + + } else if (key) { + key = arrayify(key); + + // This will be used to encrypt the wallet (as per Web3 secret storage) + var derivedKey = key.slice(0, 16); + var macPrefix = key.slice(16, 32); + + // This will be used to encrypt the mnemonic phrase (if any) + var mnemonicKey = key.slice(32, 64); + + // Get the address for this private key + var address = (new SigningKey(privateKey)).address; + + // Encrypt the private key + var counter = new aes.Counter(iv); + var aesCtr = new aes.ModeOfOperation.ctr(derivedKey, counter); + var ciphertext = utils.arrayify(aesCtr.encrypt(privateKey)); + + // Compute the message authentication code, used to check the password + var mac = utils.keccak256(utils.concat([macPrefix, ciphertext])) + + // See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition + var data = { + address: address.substring(2).toLowerCase(), + id: uuid.v4({ random: uuidRandom }), + version: 3, + Crypto: { + cipher: 'aes-128-ctr', + cipherparams: { + iv: utils.hexlify(iv).substring(2), + }, + ciphertext: utils.hexlify(ciphertext).substring(2), + kdf: 'scrypt', + kdfparams: { + salt: utils.hexlify(salt).substring(2), + n: N, + dklen: 32, + p: p, + r: r + }, + mac: mac.substring(2) + } + }; + + // If we have a mnemonic, encrypt it into the JSON wallet + if (entropy) { + var mnemonicIv = utils.randomBytes(16); + var mnemonicCounter = new aes.Counter(mnemonicIv); + var mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); + var mnemonicCiphertext = utils.arrayify(mnemonicAesCtr.encrypt(entropy)); + var now = new Date(); + var timestamp = (now.getUTCFullYear() + '-' + + zpad(now.getUTCMonth() + 1, 2) + '-' + + zpad(now.getUTCDate(), 2) + 'T' + + zpad(now.getUTCHours(), 2) + '-' + + zpad(now.getUTCMinutes(), 2) + '-' + + zpad(now.getUTCSeconds(), 2) + '.0Z' + ); + data['x-ethers'] = { + client: client, + gethFilename: ('UTC--' + timestamp + '--' + data.address), + mnemonicCounter: utils.hexlify(mnemonicIv).substring(2), + mnemonicCiphertext: utils.hexlify(mnemonicCiphertext).substring(2), + version: "0.1" + }; + } + + if (progressCallback) { progressCallback(1); } + resolve(JSON.stringify(data)); + + } else if (progressCallback) { + return progressCallback(progress); + } + }); + }); +}); + +module.exports = secretStorage; diff --git a/utils/sha2.js b/utils/sha2.js new file mode 100644 index 0000000..ebf5d51 --- /dev/null +++ b/utils/sha2.js @@ -0,0 +1,23 @@ +'use strict'; + +var hash = require('hash.js'); + +var convert = require('./convert.js'); + +function sha256(data) { + data = convert.arrayify(data); + return '0x' + (hash.sha256().update(data).digest('hex')); +} + +function sha512(data) { + data = convert.arrayify(data); + return '0x' + (hash.sha512().update(data).digest('hex')); +} + +module.exports = { + sha256: sha256, + sha512: sha512, + + createSha256: hash.sha256, + createSha512: hash.sha512, +} diff --git a/utils/signing-key.js b/utils/signing-key.js new file mode 100644 index 0000000..39c7b10 --- /dev/null +++ b/utils/signing-key.js @@ -0,0 +1,104 @@ +'use strict'; + +/** + * SigningKey + * + * + */ + +var secp256k1 = new (require('elliptic')).ec('secp256k1'); +var utils = (function() { + var convert = require('../utils/convert'); + return { + defineProperty: require('../utils/properties').defineProperty, + + arrayify: convert.arrayify, + hexlify: convert.hexlify, + padZeros: convert.padZeros, + + getAddress: require('../utils/address').getAddress, + + keccak256: require('../utils/keccak256') + }; +})(); + +var errors = require('../utils/errors'); + + +function SigningKey(privateKey) { + errors.checkNew(this, SigningKey); + + try { + privateKey = utils.arrayify(privateKey); + if (privateKey.length !== 32) { + errors.throwError('exactly 32 bytes required', errors.INVALID_ARGUMENT, { value: privateKey }); + } + } catch(error) { + var params = { arg: 'privateKey', reason: error.reason, value: '[REDACTED]' } + if (error.value) { + if(typeof(error.value.length) === 'number') { + params.length = error.value.length; + } + params.type = typeof(error.value); + } + errors.throwError('invalid private key', error.code, params); + } + + utils.defineProperty(this, 'privateKey', utils.hexlify(privateKey)) + + var keyPair = secp256k1.keyFromPrivate(privateKey); + + utils.defineProperty(this, 'publicKey', '0x' + keyPair.getPublic(true, 'hex')) + + var address = SigningKey.publicKeyToAddress('0x' + keyPair.getPublic(false, 'hex')); + utils.defineProperty(this, 'address', address) + + utils.defineProperty(this, 'signDigest', function(digest) { + var signature = keyPair.sign(utils.arrayify(digest), {canonical: true}); + var r = '0x' + signature.r.toString(16); + var s = '0x' + signature.s.toString(16); + + return { + recoveryParam: signature.recoveryParam, + r: utils.hexlify(utils.padZeros(r, 32)), + s: utils.hexlify(utils.padZeros(s, 32)) + } + }); +} + +utils.defineProperty(SigningKey, 'recover', function(digest, r, s, recoveryParam) { + var signature = { + r: utils.arrayify(r), + s: utils.arrayify(s) + }; + var publicKey = secp256k1.recoverPubKey(utils.arrayify(digest), signature, recoveryParam); + return SigningKey.publicKeyToAddress('0x' + publicKey.encode('hex', false)); +}); + + +utils.defineProperty(SigningKey, 'getPublicKey', function(value, compressed) { + value = utils.arrayify(value); + compressed = !!compressed; + + if (value.length === 32) { + var keyPair = secp256k1.keyFromPrivate(value); + return '0x' + keyPair.getPublic(compressed, 'hex'); + + } else if (value.length === 33) { + var keyPair = secp256k1.keyFromPublic(value); + return '0x' + keyPair.getPublic(compressed, 'hex'); + + } else if (value.length === 65) { + var keyPair = secp256k1.keyFromPublic(value); + return '0x' + keyPair.getPublic(compressed, 'hex'); + } + + throw new Error('invalid value'); +}); + +utils.defineProperty(SigningKey, 'publicKeyToAddress', function(publicKey) { + publicKey = '0x' + SigningKey.getPublicKey(publicKey, false).slice(4); + return utils.getAddress('0x' + utils.keccak256(publicKey).substring(26)); +}); + +module.exports = SigningKey; diff --git a/utils/solidity.js b/utils/solidity.js new file mode 100644 index 0000000..ee07d15 --- /dev/null +++ b/utils/solidity.js @@ -0,0 +1,97 @@ +'use strict'; + +var bigNumberify = require('./bignumber').bigNumberify; +var convert = require('./convert'); +var getAddress = require('./address').getAddress; +var utf8 = require('./utf8'); + +var hashKeccak256 = require('./keccak256'); +var hashSha256 = require('./sha2').sha256; + +var regexBytes = new RegExp("^bytes([0-9]+)$"); +var regexNumber = new RegExp("^(u?int)([0-9]*)$"); +var regexArray = new RegExp("^(.*)\\[([0-9]*)\\]$"); + +var Zeros = '0000000000000000000000000000000000000000000000000000000000000000'; + +function _pack(type, value, isArray) { + switch(type) { + case 'address': + if (isArray) { return convert.padZeros(value, 32); } + return convert.arrayify(value); + case 'string': + return utf8.toUtf8Bytes(value); + case 'bytes': + return convert.arrayify(value); + case 'bool': + value = (value ? '0x01': '0x00'); + if (isArray) { return convert.padZeros(value, 32); } + return convert.arrayify(value); + } + + var match = type.match(regexNumber); + if (match) { + var signed = (match[1] === 'int') + var size = parseInt(match[2] || "256") + if ((size % 8 != 0) || size === 0 || size > 256) { + throw new Error('invalid number type - ' + type); + } + + if (isArray) { size = 256; } + + value = bigNumberify(value).toTwos(size); + + return convert.padZeros(value, size / 8); + } + + match = type.match(regexBytes); + if (match) { + var size = match[1]; + if (size != parseInt(size) || size === 0 || size > 32) { + throw new Error('invalid number type - ' + type); + } + size = parseInt(size); + if (convert.arrayify(value).byteLength !== size) { throw new Error('invalid value for ' + type); } + if (isArray) { return (value + Zeros).substring(0, 66); } + return value; + } + + match = type.match(regexArray); + if (match) { + var baseType = match[1]; + var count = parseInt(match[2] || value.length); + if (count != value.length) { throw new Error('invalid value for ' + type); } + var result = []; + value.forEach(function(value) { + value = _pack(baseType, value, true); + result.push(value); + }); + return convert.concat(result); + } + + throw new Error('unknown type - ' + type); +} + +function pack(types, values) { + if (types.length != values.length) { throw new Error('type/value count mismatch'); } + var tight = []; + types.forEach(function(type, index) { + tight.push(_pack(type, values[index])); + }); + return convert.hexlify(convert.concat(tight)); +} + +function keccak256(types, values) { + return hashKeccak256(pack(types, values)); +} + +function sha256(types, values) { + return hashSha256(pack(types, values)); +} + +module.exports = { + pack: pack, + + keccak256: keccak256, + sha256: sha256, +} diff --git a/utils/throw-error.js b/utils/throw-error.js new file mode 100644 index 0000000..3ea1135 --- /dev/null +++ b/utils/throw-error.js @@ -0,0 +1,11 @@ +'use strict'; + +function throwError(message, params) { + var error = new Error(message); + for (var key in params) { + error[key] = params[key]; + } + throw error; +} + +module.exports = throwError; diff --git a/utils/units.js b/utils/units.js new file mode 100644 index 0000000..b793c08 --- /dev/null +++ b/utils/units.js @@ -0,0 +1,148 @@ +var bigNumberify = require('./bignumber.js').bigNumberify; +var throwError = require('./throw-error'); + +var zero = new bigNumberify(0); +var negative1 = new bigNumberify(-1); + +var names = [ + 'wei', + 'kwei', + 'Mwei', + 'Gwei', + 'szabo', + 'finny', + 'ether', +]; + +var getUnitInfo = (function() { + var unitInfos = {}; + + function getUnitInfo(value) { + return { + decimals: value.length - 1, + tenPower: bigNumberify(value) + }; + } + + // Cache the common units + var value = '1'; + names.forEach(function(name) { + var info = getUnitInfo(value); + unitInfos[name.toLowerCase()] = info; + unitInfos[String(info.decimals)] = info; + value += '000'; + }); + + return function(name) { + // Try the cache + var info = unitInfos[String(name).toLowerCase()]; + + if (!info && typeof(name) === 'number' && parseInt(name) == name && name >= 0 && name <= 256) { + var value = '1'; + for (var i = 0; i < name; i++) { value += '0'; } + info = getUnitInfo(value); + } + + // Make sure we got something + if (!info) { throwError('invalid unitType', { unitType: name }); } + + return info; + } +})(); + +function formatUnits(value, unitType, options) { + if (typeof(unitType) === 'object' && !options) { + options = unitType; + unitType = undefined; + } + + if (unitType == null) { unitType = 18; } + var unitInfo = getUnitInfo(unitType); + + // Make sure wei is a big number (convert as necessary) + value = bigNumberify(value); + + if (!options) { options = {}; } + + var negative = value.lt(zero); + if (negative) { value = value.mul(negative1); } + + var fraction = value.mod(unitInfo.tenPower).toString(10); + while (fraction.length < unitInfo.decimals) { fraction = '0' + fraction; } + + // Strip off trailing zeros (but keep one if would otherwise be bare decimal point) + if (!options.pad) { + fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]; + } + + var whole = value.div(unitInfo.tenPower).toString(10); + + if (options.commify) { + whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + } + + var value = whole + '.' + fraction; + + if (negative) { value = '-' + value; } + + return value; +} + +function parseUnits(value, unitType) { + if (unitType == null) { unitType = 18; } + var unitInfo = getUnitInfo(unitType); + + if (typeof(value) !== 'string' || !value.match(/^-?[0-9.,]+$/)) { + throwError('invalid value', { input: value }); + } + + // Remove commas + var value = value.replace(/,/g,''); + + // Is it negative? + var negative = (value.substring(0, 1) === '-'); + if (negative) { value = value.substring(1); } + + if (value === '.') { throwError('invalid value', { input: value }); } + + // Split it into a whole and fractional part + var comps = value.split('.'); + if (comps.length > 2) { throwError('too many decimal points', { input: value }); } + + var whole = comps[0], fraction = comps[1]; + if (!whole) { whole = '0'; } + if (!fraction) { fraction = '0'; } + + // Prevent underflow + if (fraction.length > unitInfo.decimals) { + throwError('too many decimal places', { input: value, decimals: fraction.length }); + } + + // Fully pad the string with zeros to get to wei + while (fraction.length < unitInfo.decimals) { fraction += '0'; } + + whole = bigNumberify(whole); + fraction = bigNumberify(fraction); + + var wei = (whole.mul(unitInfo.tenPower)).add(fraction); + + if (negative) { wei = wei.mul(negative1); } + + return wei; +} + +function formatEther(wei, options) { + return formatUnits(wei, 18, options); +} + +function parseEther(ether) { + return parseUnits(ether, 18); +} + +module.exports = { + formatEther: formatEther, + parseEther: parseEther, + + formatUnits: formatUnits, + parseUnits: parseUnits, +} diff --git a/utils/utf8.js b/utils/utf8.js new file mode 100644 index 0000000..b444bf8 --- /dev/null +++ b/utils/utf8.js @@ -0,0 +1,113 @@ + +var convert = require('./convert.js'); + +// http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array +function utf8ToBytes(str) { + var result = []; + var offset = 0; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if (c < 128) { + result[offset++] = c; + } else if (c < 2048) { + result[offset++] = (c >> 6) | 192; + result[offset++] = (c & 63) | 128; + } else if (((c & 0xFC00) == 0xD800) && (i + 1) < str.length && ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) { + // Surrogate Pair + c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF); + result[offset++] = (c >> 18) | 240; + result[offset++] = ((c >> 12) & 63) | 128; + result[offset++] = ((c >> 6) & 63) | 128; + result[offset++] = (c & 63) | 128; + } else { + result[offset++] = (c >> 12) | 224; + result[offset++] = ((c >> 6) & 63) | 128; + result[offset++] = (c & 63) | 128; + } + } + + return convert.arrayify(result); +}; + + +// http://stackoverflow.com/questions/13356493/decode-utf-8-with-javascript#13691499 +function bytesToUtf8(bytes) { + bytes = convert.arrayify(bytes); + + var result = ''; + var i = 0; + + // Invalid bytes are ignored + while(i < bytes.length) { + var c = bytes[i++]; + if (c >> 7 == 0) { + // 0xxx xxxx + result += String.fromCharCode(c); + continue; + } + + // Invalid starting byte + if (c >> 6 == 0x02) { continue; } + + // Multibyte; how many bytes left for thus character? + var extraLength = null; + if (c >> 5 == 0x06) { + extraLength = 1; + } else if (c >> 4 == 0x0e) { + extraLength = 2; + } else if (c >> 3 == 0x1e) { + extraLength = 3; + } else if (c >> 2 == 0x3e) { + extraLength = 4; + } else if (c >> 1 == 0x7e) { + extraLength = 5; + } else { + continue; + } + + // Do we have enough bytes in our data? + if (i + extraLength > bytes.length) { + + // If there is an invalid unprocessed byte, try to continue + for (; i < bytes.length; i++) { + if (bytes[i] >> 6 != 0x02) { break; } + } + if (i != bytes.length) continue; + + // All leftover bytes are valid. + return result; + } + + // Remove the UTF-8 prefix from the char (res) + var res = c & ((1 << (8 - extraLength - 1)) - 1); + + var count; + for (count = 0; count < extraLength; count++) { + var nextChar = bytes[i++]; + + // Is the char valid multibyte part? + if (nextChar >> 6 != 0x02) {break;}; + res = (res << 6) | (nextChar & 0x3f); + } + + if (count != extraLength) { + i--; + continue; + } + + if (res <= 0xffff) { + result += String.fromCharCode(res); + continue; + } + + res -= 0x10000; + result += String.fromCharCode(((res >> 10) & 0x3ff) + 0xd800, (res & 0x3ff) + 0xdc00); + } + + return result; +} + +module.exports = { + toUtf8Bytes: utf8ToBytes, + toUtf8String: bytesToUtf8, +}; diff --git a/utils/words.json b/utils/words.json new file mode 100644 index 0000000..728deaf --- /dev/null +++ b/utils/words.json @@ -0,0 +1 @@ +"AbandonAbilityAbleAboutAboveAbsentAbsorbAbstractAbsurdAbuseAccessAccidentAccountAccuseAchieveAcidAcousticAcquireAcrossActActionActorActressActualAdaptAddAddictAddressAdjustAdmitAdultAdvanceAdviceAerobicAffairAffordAfraidAgainAgeAgentAgreeAheadAimAirAirportAisleAlarmAlbumAlcoholAlertAlienAllAlleyAllowAlmostAloneAlphaAlreadyAlsoAlterAlwaysAmateurAmazingAmongAmountAmusedAnalystAnchorAncientAngerAngleAngryAnimalAnkleAnnounceAnnualAnotherAnswerAntennaAntiqueAnxietyAnyApartApologyAppearAppleApproveAprilArchArcticAreaArenaArgueArmArmedArmorArmyAroundArrangeArrestArriveArrowArtArtefactArtistArtworkAskAspectAssaultAssetAssistAssumeAsthmaAthleteAtomAttackAttendAttitudeAttractAuctionAuditAugustAuntAuthorAutoAutumnAverageAvocadoAvoidAwakeAwareAwayAwesomeAwfulAwkwardAxisBabyBachelorBaconBadgeBagBalanceBalconyBallBambooBananaBannerBarBarelyBargainBarrelBaseBasicBasketBattleBeachBeanBeautyBecauseBecomeBeefBeforeBeginBehaveBehindBelieveBelowBeltBenchBenefitBestBetrayBetterBetweenBeyondBicycleBidBikeBindBiologyBirdBirthBitterBlackBladeBlameBlanketBlastBleakBlessBlindBloodBlossomBlouseBlueBlurBlushBoardBoatBodyBoilBombBoneBonusBookBoostBorderBoringBorrowBossBottomBounceBoxBoyBracketBrainBrandBrassBraveBreadBreezeBrickBridgeBriefBrightBringBriskBroccoliBrokenBronzeBroomBrotherBrownBrushBubbleBuddyBudgetBuffaloBuildBulbBulkBulletBundleBunkerBurdenBurgerBurstBusBusinessBusyButterBuyerBuzzCabbageCabinCableCactusCageCakeCallCalmCameraCampCanCanalCancelCandyCannonCanoeCanvasCanyonCapableCapitalCaptainCarCarbonCardCargoCarpetCarryCartCaseCashCasinoCastleCasualCatCatalogCatchCategoryCattleCaughtCauseCautionCaveCeilingCeleryCementCensusCenturyCerealCertainChairChalkChampionChangeChaosChapterChargeChaseChatCheapCheckCheeseChefCherryChestChickenChiefChildChimneyChoiceChooseChronicChuckleChunkChurnCigarCinnamonCircleCitizenCityCivilClaimClapClarifyClawClayCleanClerkCleverClickClientCliffClimbClinicClipClockClogCloseClothCloudClownClubClumpClusterClutchCoachCoastCoconutCodeCoffeeCoilCoinCollectColorColumnCombineComeComfortComicCommonCompanyConcertConductConfirmCongressConnectConsiderControlConvinceCookCoolCopperCopyCoralCoreCornCorrectCostCottonCouchCountryCoupleCourseCousinCoverCoyoteCrackCradleCraftCramCraneCrashCraterCrawlCrazyCreamCreditCreekCrewCricketCrimeCrispCriticCropCrossCrouchCrowdCrucialCruelCruiseCrumbleCrunchCrushCryCrystalCubeCultureCupCupboardCuriousCurrentCurtainCurveCushionCustomCuteCycleDadDamageDampDanceDangerDaringDashDaughterDawnDayDealDebateDebrisDecadeDecemberDecideDeclineDecorateDecreaseDeerDefenseDefineDefyDegreeDelayDeliverDemandDemiseDenialDentistDenyDepartDependDepositDepthDeputyDeriveDescribeDesertDesignDeskDespairDestroyDetailDetectDevelopDeviceDevoteDiagramDialDiamondDiaryDiceDieselDietDifferDigitalDignityDilemmaDinnerDinosaurDirectDirtDisagreeDiscoverDiseaseDishDismissDisorderDisplayDistanceDivertDivideDivorceDizzyDoctorDocumentDogDollDolphinDomainDonateDonkeyDonorDoorDoseDoubleDoveDraftDragonDramaDrasticDrawDreamDressDriftDrillDrinkDripDriveDropDrumDryDuckDumbDuneDuringDustDutchDutyDwarfDynamicEagerEagleEarlyEarnEarthEasilyEastEasyEchoEcologyEconomyEdgeEditEducateEffortEggEightEitherElbowElderElectricElegantElementElephantElevatorEliteElseEmbarkEmbodyEmbraceEmergeEmotionEmployEmpowerEmptyEnableEnactEndEndlessEndorseEnemyEnergyEnforceEngageEngineEnhanceEnjoyEnlistEnoughEnrichEnrollEnsureEnterEntireEntryEnvelopeEpisodeEqualEquipEraEraseErodeErosionErrorEruptEscapeEssayEssenceEstateEternalEthicsEvidenceEvilEvokeEvolveExactExampleExcessExchangeExciteExcludeExcuseExecuteExerciseExhaustExhibitExileExistExitExoticExpandExpectExpireExplainExposeExpressExtendExtraEyeEyebrowFabricFaceFacultyFadeFaintFaithFallFalseFameFamilyFamousFanFancyFantasyFarmFashionFatFatalFatherFatigueFaultFavoriteFeatureFebruaryFederalFeeFeedFeelFemaleFenceFestivalFetchFeverFewFiberFictionFieldFigureFileFilmFilterFinalFindFineFingerFinishFireFirmFirstFiscalFishFitFitnessFixFlagFlameFlashFlatFlavorFleeFlightFlipFloatFlockFloorFlowerFluidFlushFlyFoamFocusFogFoilFoldFollowFoodFootForceForestForgetForkFortuneForumForwardFossilFosterFoundFoxFragileFrameFrequentFreshFriendFringeFrogFrontFrostFrownFrozenFruitFuelFunFunnyFurnaceFuryFutureGadgetGainGalaxyGalleryGameGapGarageGarbageGardenGarlicGarmentGasGaspGateGatherGaugeGazeGeneralGeniusGenreGentleGenuineGestureGhostGiantGiftGiggleGingerGiraffeGirlGiveGladGlanceGlareGlassGlideGlimpseGlobeGloomGloryGloveGlowGlueGoatGoddessGoldGoodGooseGorillaGospelGossipGovernGownGrabGraceGrainGrantGrapeGrassGravityGreatGreenGridGriefGritGroceryGroupGrowGruntGuardGuessGuideGuiltGuitarGunGymHabitHairHalfHammerHamsterHandHappyHarborHardHarshHarvestHatHaveHawkHazardHeadHealthHeartHeavyHedgehogHeightHelloHelmetHelpHenHeroHiddenHighHillHintHipHireHistoryHobbyHockeyHoldHoleHolidayHollowHomeHoneyHoodHopeHornHorrorHorseHospitalHostHotelHourHoverHubHugeHumanHumbleHumorHundredHungryHuntHurdleHurryHurtHusbandHybridIceIconIdeaIdentifyIdleIgnoreIllIllegalIllnessImageImitateImmenseImmuneImpactImposeImproveImpulseInchIncludeIncomeIncreaseIndexIndicateIndoorIndustryInfantInflictInformInhaleInheritInitialInjectInjuryInmateInnerInnocentInputInquiryInsaneInsectInsideInspireInstallIntactInterestIntoInvestInviteInvolveIronIslandIsolateIssueItemIvoryJacketJaguarJarJazzJealousJeansJellyJewelJobJoinJokeJourneyJoyJudgeJuiceJumpJungleJuniorJunkJustKangarooKeenKeepKetchupKeyKickKidKidneyKindKingdomKissKitKitchenKiteKittenKiwiKneeKnifeKnockKnowLabLabelLaborLadderLadyLakeLampLanguageLaptopLargeLaterLatinLaughLaundryLavaLawLawnLawsuitLayerLazyLeaderLeafLearnLeaveLectureLeftLegLegalLegendLeisureLemonLendLengthLensLeopardLessonLetterLevelLiarLibertyLibraryLicenseLifeLiftLightLikeLimbLimitLinkLionLiquidListLittleLiveLizardLoadLoanLobsterLocalLockLogicLonelyLongLoopLotteryLoudLoungeLoveLoyalLuckyLuggageLumberLunarLunchLuxuryLyricsMachineMadMagicMagnetMaidMailMainMajorMakeMammalManManageMandateMangoMansionManualMapleMarbleMarchMarginMarineMarketMarriageMaskMassMasterMatchMaterialMathMatrixMatterMaximumMazeMeadowMeanMeasureMeatMechanicMedalMediaMelodyMeltMemberMemoryMentionMenuMercyMergeMeritMerryMeshMessageMetalMethodMiddleMidnightMilkMillionMimicMindMinimumMinorMinuteMiracleMirrorMiseryMissMistakeMixMixedMixtureMobileModelModifyMomMomentMonitorMonkeyMonsterMonthMoonMoralMoreMorningMosquitoMotherMotionMotorMountainMouseMoveMovieMuchMuffinMuleMultiplyMuscleMuseumMushroomMusicMustMutualMyselfMysteryMythNaiveNameNapkinNarrowNastyNationNatureNearNeckNeedNegativeNeglectNeitherNephewNerveNestNetNetworkNeutralNeverNewsNextNiceNightNobleNoiseNomineeNoodleNormalNorthNoseNotableNoteNothingNoticeNovelNowNuclearNumberNurseNutOakObeyObjectObligeObscureObserveObtainObviousOccurOceanOctoberOdorOffOfferOfficeOftenOilOkayOldOliveOlympicOmitOnceOneOnionOnlineOnlyOpenOperaOpinionOpposeOptionOrangeOrbitOrchardOrderOrdinaryOrganOrientOriginalOrphanOstrichOtherOutdoorOuterOutputOutsideOvalOvenOverOwnOwnerOxygenOysterOzonePactPaddlePagePairPalacePalmPandaPanelPanicPantherPaperParadeParentParkParrotPartyPassPatchPathPatientPatrolPatternPausePavePaymentPeacePeanutPearPeasantPelicanPenPenaltyPencilPeoplePepperPerfectPermitPersonPetPhonePhotoPhrasePhysicalPianoPicnicPicturePiecePigPigeonPillPilotPinkPioneerPipePistolPitchPizzaPlacePlanetPlasticPlatePlayPleasePledgePluckPlugPlungePoemPoetPointPolarPolePolicePondPonyPoolPopularPortionPositionPossiblePostPotatoPotteryPovertyPowderPowerPracticePraisePredictPreferPreparePresentPrettyPreventPricePridePrimaryPrintPriorityPrisonPrivatePrizeProblemProcessProduceProfitProgramProjectPromoteProofPropertyProsperProtectProudProvidePublicPuddingPullPulpPulsePumpkinPunchPupilPuppyPurchasePurityPurposePursePushPutPuzzlePyramidQualityQuantumQuarterQuestionQuickQuitQuizQuoteRabbitRaccoonRaceRackRadarRadioRailRainRaiseRallyRampRanchRandomRangeRapidRareRateRatherRavenRawRazorReadyRealReasonRebelRebuildRecallReceiveRecipeRecordRecycleReduceReflectReformRefuseRegionRegretRegularRejectRelaxReleaseReliefRelyRemainRememberRemindRemoveRenderRenewRentReopenRepairRepeatReplaceReportRequireRescueResembleResistResourceResponseResultRetireRetreatReturnReunionRevealReviewRewardRhythmRibRibbonRiceRichRideRidgeRifleRightRigidRingRiotRippleRiskRitualRivalRiverRoadRoastRobotRobustRocketRomanceRoofRookieRoomRoseRotateRoughRoundRouteRoyalRubberRudeRugRuleRunRunwayRuralSadSaddleSadnessSafeSailSaladSalmonSalonSaltSaluteSameSampleSandSatisfySatoshiSauceSausageSaveSayScaleScanScareScatterSceneSchemeSchoolScienceScissorsScorpionScoutScrapScreenScriptScrubSeaSearchSeasonSeatSecondSecretSectionSecuritySeedSeekSegmentSelectSellSeminarSeniorSenseSentenceSeriesServiceSessionSettleSetupSevenShadowShaftShallowShareShedShellSheriffShieldShiftShineShipShiverShockShoeShootShopShortShoulderShoveShrimpShrugShuffleShySiblingSickSideSiegeSightSignSilentSilkSillySilverSimilarSimpleSinceSingSirenSisterSituateSixSizeSkateSketchSkiSkillSkinSkirtSkullSlabSlamSleepSlenderSliceSlideSlightSlimSloganSlotSlowSlushSmallSmartSmileSmokeSmoothSnackSnakeSnapSniffSnowSoapSoccerSocialSockSodaSoftSolarSoldierSolidSolutionSolveSomeoneSongSoonSorrySortSoulSoundSoupSourceSouthSpaceSpareSpatialSpawnSpeakSpecialSpeedSpellSpendSphereSpiceSpiderSpikeSpinSpiritSplitSpoilSponsorSpoonSportSpotSpraySpreadSpringSpySquareSqueezeSquirrelStableStadiumStaffStageStairsStampStandStartStateStaySteakSteelStemStepStereoStickStillStingStockStomachStoneStoolStoryStoveStrategyStreetStrikeStrongStruggleStudentStuffStumbleStyleSubjectSubmitSubwaySuccessSuchSuddenSufferSugarSuggestSuitSummerSunSunnySunsetSuperSupplySupremeSureSurfaceSurgeSurpriseSurroundSurveySuspectSustainSwallowSwampSwapSwarmSwearSweetSwiftSwimSwingSwitchSwordSymbolSymptomSyrupSystemTableTackleTagTailTalentTalkTankTapeTargetTaskTasteTattooTaxiTeachTeamTellTenTenantTennisTentTermTestTextThankThatThemeThenTheoryThereTheyThingThisThoughtThreeThriveThrowThumbThunderTicketTideTigerTiltTimberTimeTinyTipTiredTissueTitleToastTobaccoTodayToddlerToeTogetherToiletTokenTomatoTomorrowToneTongueTonightToolToothTopTopicToppleTorchTornadoTortoiseTossTotalTouristTowardTowerTownToyTrackTradeTrafficTragicTrainTransferTrapTrashTravelTrayTreatTreeTrendTrialTribeTrickTriggerTrimTripTrophyTroubleTruckTrueTrulyTrumpetTrustTruthTryTubeTuitionTumbleTunaTunnelTurkeyTurnTurtleTwelveTwentyTwiceTwinTwistTwoTypeTypicalUglyUmbrellaUnableUnawareUncleUncoverUnderUndoUnfairUnfoldUnhappyUniformUniqueUnitUniverseUnknownUnlockUntilUnusualUnveilUpdateUpgradeUpholdUponUpperUpsetUrbanUrgeUsageUseUsedUsefulUselessUsualUtilityVacantVacuumVagueValidValleyValveVanVanishVaporVariousVastVaultVehicleVelvetVendorVentureVenueVerbVerifyVersionVeryVesselVeteranViableVibrantViciousVictoryVideoViewVillageVintageViolinVirtualVirusVisaVisitVisualVitalVividVocalVoiceVoidVolcanoVolumeVoteVoyageWageWagonWaitWalkWallWalnutWantWarfareWarmWarriorWashWaspWasteWaterWaveWayWealthWeaponWearWeaselWeatherWebWeddingWeekendWeirdWelcomeWestWetWhaleWhatWheatWheelWhenWhereWhipWhisperWideWidthWifeWildWillWinWindowWineWingWinkWinnerWinterWireWisdomWiseWishWitnessWolfWomanWonderWoodWoolWordWorkWorldWorryWorthWrapWreckWrestleWristWriteWrongYardYearYellowYouYoungYouthZebraZeroZoneZoo"