commit 16cca302b79b588108fab984a5d2e4aba00665a7 Author: Luk.Lu Date: Tue Oct 9 23:03:05 2018 +0800 首次放到 git 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"