Compare commits

..

95 Commits

Author SHA1 Message Date
ricmoo
dc2583ddd8 Updating dist. 2016-10-05 00:12:15 +08:00
ricmoo
3be962f09d Made secret-storage more closely resemble geth. 2016-10-05 00:06:05 +08:00
ricmoo
736b08e016 Updated parseTransaction to format parameters more meaningfully. 2016-09-16 14:46:19 +08:00
ricmoo
d8013cae37 Added transaction parsing with address recovery. 2016-09-16 14:08:36 +08:00
ricmoo
b26b1b9c53 Moved getContractAddress to utils. 2016-08-23 22:06:26 -04:00
ricmoo
1c9c7b7a7d Refactored some internal libraries. 2016-08-23 20:24:18 -04:00
ricmoo
a9aaeefe24 Moved address functions to utils (things without need ofr signing do not need all the extra requires). 2016-08-23 20:12:12 -04:00
ricmoo
8257e3f885 Moved ether parsing/formatting into its own library. 2016-08-23 19:35:19 -04:00
ricmoo
965c987761 Updated docs for promise-based secret storage. 2016-08-15 23:27:10 -04:00
ricmoo
142e5276fe Moved encrypt, decrypt and brain wallet to use promises (which greatly improves nested progress callback, approx 4x performance). 2016-08-15 23:04:02 -04:00
ricmoo
351d1a2dad Added github repo to package.json. 2016-08-10 14:30:33 -04:00
ricmoo
72424ea2d2 Fixed typo in wallet demo. 2016-08-08 17:12:27 -04:00
ricmoo
1e703bfdb4 Version Bump (patch). 2016-08-08 14:34:55 -04:00
ricmoo
dee0a4bf3e Added raw private key and made homestead default network. 2016-08-08 00:18:01 -04:00
ricmoo
9c1d6051fb Fixed empty transaction field bug. 2016-08-07 15:18:42 -04:00
ricmoo
3c67736cbb Fixed randomish bug. 2016-08-07 15:17:53 -04:00
ricmoo
c697a5bc81 Version bump (patch). 2016-08-05 16:15:50 -04:00
ricmoo
0e9df5cb76 Fixed bug when passing in hex string to formatEther. 2016-08-05 16:14:09 -04:00
ricmoo
7a37dd6949 Added installing section and npm badge to docs. 2016-08-05 03:53:29 -04:00
ricmoo
cecefd416d Small changes to docs. 2016-08-05 03:40:31 -04:00
ricmoo
79f9047c21 Added wallet provider test. 2016-08-05 03:16:13 -04:00
ricmoo
f9f7469ccf Fixed variable masking issue in exaples in docs. 2016-08-05 03:06:36 -04:00
ricmoo
1e5933eb45 Fixed test cases that were being missed. 2016-08-05 03:04:17 -04:00
ricmoo
a4e1f531b7 Etherscan now supports estiateGas and getGasPrice (thanks Matt!). 2016-08-05 02:56:24 -04:00
ricmoo
0a38d14930 Updated dist. 2016-08-04 21:38:05 -04:00
ricmoo
4e7f2fa3d4 Allow wei to be passed in as a hex string. 2016-08-04 21:37:33 -04:00
ricmoo
4b99bb579c Fixed getContractAddress function 2016-08-04 21:37:15 -04:00
ricmoo
0b59519c19 Version bump (patch). 2016-08-04 20:29:55 -04:00
ricmoo
4d18119ccf Updated dist. 2016-08-04 20:29:20 -04:00
ricmoo
4af236d3ac Moved setImmediate shim to wallet. 2016-08-04 20:28:55 -04:00
ricmoo
eddb9c28a8 Moved setImmediate shim into wallet. 2016-08-04 20:27:21 -04:00
ricmoo
40a6fdd95e Removed web3 from splitter. 2016-08-04 20:12:50 -04:00
ricmoo
3f80001e4d On failed RPC, include original error and data. 2016-08-04 17:53:44 -04:00
ricmoo
89082164ce Version bump (patch). 2016-08-04 04:00:02 -04:00
ricmoo
9df7c2f37a Fixed etherscan production url. 2016-08-04 03:58:48 -04:00
ricmoo
840f510bf1 Added documentation note for why gas price is hard-coded. 2016-08-04 03:43:59 -04:00
ricmoo
1bd95b127b Added wallet example. 2016-08-04 03:21:49 -04:00
ricmoo
97154e49a6 Version bump (patch). 2016-08-04 03:20:37 -04:00
ricmoo
db757b5bbb Fixed wrong default gasLimit bug and clearing provider. 2016-08-04 03:20:01 -04:00
ricmoo
af62f54042 Added etherscan example to docs. 2016-08-03 19:41:46 -04:00
ricmoo
1b0dc18bdc Abstracted providers and added etherscan provider. 2016-08-03 19:36:44 -04:00
ricmoo
1db36b3132 Version bump (patch). 2016-08-03 02:27:56 -04:00
ricmoo
805879a213 Refactored provider to parse the response. 2016-08-03 02:26:36 -04:00
ricmoo
e1157f3a4c Updated documentation with Provider API and related methods. 2016-08-02 17:49:11 -04:00
ricmoo
46e0866410 Updated dist. 2016-08-02 17:46:38 -04:00
ricmoo
621c1ee74e Removed duplicate functions. 2016-08-02 17:46:20 -04:00
ricmoo
b3f9070b39 Added full provider API including estimate proxy. 2016-08-02 17:43:18 -04:00
ricmoo
14b15dcf5a Added provider standard methods. 2016-08-02 17:41:28 -04:00
ricmoo
e4c62d2939 Updated test info in the docs. 2016-08-02 01:12:04 -04:00
ricmoo
b50c0bd615 Added license file. 2016-08-02 01:11:43 -04:00
ricmoo
e55ee253ab Cleaned up solidity test cases. 2016-08-01 20:09:34 -04:00
ricmoo
f81fa6e4f5 Version bump (patch; still very beta). 2016-08-01 20:06:54 -04:00
ricmoo
4785c650f3 Upgraded some packages. 2016-08-01 19:39:54 -04:00
ricmoo
be99fa858e Removed debug logging. 2016-08-01 19:39:40 -04:00
ricmoo
196d4f6659 Fixed docs. at bit 2016-08-01 19:34:03 -04:00
ricmoo
1233233c6c Removed debugging logging. 2016-08-01 19:33:21 -04:00
ricmoo
742bcc753f Added brainwallet test case. 2016-08-01 19:32:57 -04:00
ricmoo
839c2a10aa Added summonBrainWallet function. 2016-08-01 19:25:18 -04:00
ricmoo
b298d41c22 Added dependencies for test cases. 2016-08-01 18:37:04 -04:00
ricmoo
c70a9f61cb Added some more info to contract tests that require RPC running. 2016-08-01 18:36:33 -04:00
ricmoo
6440d8e69d Allow running individual unit tests. 2016-08-01 18:34:12 -04:00
ricmoo
97dcd4d450 Fixed testcase generation for soidity parameter encoding/decoding. 2016-08-01 18:31:51 -04:00
ricmoo
b8d4514dd0 Fixed contract interface decoding numbers. 2016-08-01 18:26:12 -04:00
ricmoo
0ceee56de3 Fixed contract interface coder for numbers. 2016-08-01 17:57:00 -04:00
ricmoo
024cc1806f Re-ordered some parameters, fixed typo. 2016-08-01 14:14:53 -04:00
ricmoo
5e421d2636 Added ether string parsing and formatting. 2016-07-29 20:06:58 -04:00
ricmoo
f71ea72d54 Added https proxy for splitter demo. 2016-07-29 15:40:15 -04:00
ricmoo
d65379f6c4 Added splitter demo app. 2016-07-29 04:46:59 -04:00
ricmoo
06281e85c4 Fixed RPC method calls. 2016-07-29 04:45:46 -04:00
ricmoo
5dc2621a32 Fixed type in randomish. 2016-07-27 03:09:11 -04:00
ricmoo
c024f88ce1 Updated dist to inlcude randomish. 2016-07-27 03:03:14 -04:00
ricmoo
b5e6ac7db9 Varialbe name changes. 2016-07-27 03:02:30 -04:00
ricmoo
60732f8243 Added gitignore. 2016-07-27 03:01:52 -04:00
ricmoo
4a557349a3 Added testcase for encrypting a wallet. 2016-07-27 03:01:37 -04:00
ricmoo
5bf0c310d3 Added randomish library. 2016-07-27 02:53:40 -04:00
ricmoo
99ec103e97 Removed some debugging info. 2016-07-27 02:52:21 -04:00
ricmoo
829505d548 Split up test cases. 2016-07-26 17:58:17 -04:00
ricmoo
5c9ee7f2a5 Added beginning of RPC providers. 2016-07-26 17:57:11 -04:00
ricmoo
fa0d54966d Added use strict to all files. 2016-07-25 03:55:16 -04:00
ricmoo
401dd5162d Updated dist and version bump. 2016-07-23 03:29:31 -04:00
ricmoo
2dee42449a Updated testcases to include secret storage. 2016-07-23 03:28:19 -04:00
ricmoo
224e8aff07 Allow signing key to be passed into wallet and expose private key. 2016-07-23 03:27:56 -04:00
ricmoo
b5710cd710 Added Secret Storage JSON Wallet support. 2016-07-23 03:27:14 -04:00
ricmoo
8033a8f94c Fixed double-0x bug in Interface. 2016-07-22 14:49:26 -04:00
ricmoo
4bcef804b6 Updated testcases. 2016-07-22 14:49:03 -04:00
ricmoo
66a5b0ecdc Expose private key in a signing key. 2016-07-22 02:43:19 -04:00
ricmoo
2605e7de40 Moved to proper tet framework. 2016-07-22 02:43:03 -04:00
ricmoo
f50f171223 Reuse existing functions. 2016-07-21 17:07:40 -04:00
ricmoo
7626b541f5 Refactor. 2016-07-21 17:06:42 -04:00
ricmoo
24c7fc533a Refactored wallet and utilities. 2016-07-21 17:03:32 -04:00
ricmoo
21fdfe9581 Moved all Contract code to lib/contract.js. 2016-07-21 04:21:44 -04:00
ricmoo
ca8ee1c0e3 Updated documentation. 2016-07-21 04:20:31 -04:00
ricmoo
0980b864fa Added start of contract ABI library. 2016-07-20 18:06:03 -04:00
ricmoo
d0a822f02a Added isHexString and sha256 to utils. 2016-07-20 18:03:46 -04:00
ricmoo
4dbce955ee Added sha256 to utils. 2016-07-20 18:03:05 -04:00
40 changed files with 8167 additions and 19836 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

22
LICENSE.txt Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

293
README.md
View File

@@ -1,19 +1,46 @@
ethers-wallet
=============
Complete Ethereum wallet implementation in JavaScript.
[![npm version](https://badge.fury.io/js/ethers-wallet.svg)](https://badge.fury.io/js/ethers-wallet)
Features
- Keep your private keys in the browser
- Small (~155kb compressed; hopefully under 100kb soon)
- MIT licensed (with a few exceptions, which we are migrating off of; see below)
Complete Ethereum wallet implementation and utilities in JavaScript.
*NOTE: This is still very beta; please only use it on the testnet for now, or with VERY small amounts of ether on the livenet that you are willing to lose due to bugs.*
**Features:**
- Keep your private keys in your browser
- Import and export JSON wallets (Geth and crowdsale) and brain wallets
- Meta-classes create JavaScript objects from any contract ABI
- Connect to Ethereum nodes over RPC, injected Web3 or [Etherscan](https://etherscan.io)
- Small (~100kb compressed; 290kb uncompressed)
- MIT licensed (with one exception, which we are migrating off of; see below)
**NOTE: This is still very beta; please only use it on the testnet for now, or with VERY small amounts of ether on the livenet that you are willing to lose due to bugs.**
Wallet API
Installing
----------
To use in a browser:
```html
<script type="text/javascript" src="https://rawgit.com/ethers-io/ethers-wallet/master/dist/ethers-wallet.min.js"></script>
```
To use in [node.js](https://nodejs.org/):
```
npm install ethers-wallet
```
API
---
### Wallet API
An *Ethereum* wallet wraps a cryptographic private key, which is used to sign transactions and control the ether located at the wallet's address. These transactions can then be broadcast to the *Ethereum* network.
```javascript
// A private key can be specified as a 32 byte buffer or hexidecimal string
var privateKey = new Wallet.utils.Buffer([
@@ -29,17 +56,36 @@ var privateKey = '0x314159265358979323846264338327950288419716939937510582097494
// Create a wallet object
var wallet = new Wallet(privateKey)
// Wallet privateKey
console.log(wallet.privateKey)
/// "0x3141592653589793238462643383279502884197169399375105820974944592"
// Wallet address
console.log(wallet.address)
/// "0x7357589f8e367c2C31F51242fB77B350A11830F3"
// Sign transactions
wallet.sign({
to: "0x06B5955A67D827CDF91823E3bB8F069e6c89c1D6",
gasLimit: 3000000,
gasPrice: "0x1000",
value: "0x1000"
})
```
### Converting addresses
Addresses come in various forms, and it is often useful to convert between them. You can pass any valid address into any function, and the library will convert it internally as needed. The address types are:
- **hexidecimal** - 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef (all letters must be either lower case or uppercase; no mixed case, as this implies a checksum address)
- **ICAP** - XE49Q0EPSW7XTS5PRIE9226HRPOO69XRVU7 (uses the International Bank Account Number format)
- **checksum** - 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF (notice the case is adjusted encoding checkum information)
```javascript
// ICAP Addresses
Wallet.getIcapAddress(wallet.address)
/// "XE39DH16QOXYG5JY9BYY6JGZW8ORUPBX71V"
Wallet.getIcapAddress("XE39DH16QOXYG5JY9BYY6JGZW8ORUPBX71V")
/// "XE39DH16QOXYG5JY9BYY6JGZW8ORUPBX71V"
// Get checksummed address (from ICAP)
Wallet.getAddress("XE39DH16QOXYG5JY9BYY6JGZW8ORUPBX71V")
/// "0x7357589f8e367c2C31F51242fB77B350A11830F3"
@@ -50,27 +96,187 @@ Wallet.getAddress("0x7357589f8e367c2c31f51242fb77b350a11830f3")
// Detect address checksum errors (notice the last "f" should be lowercase)
Wallet.getAddress('0x7357589f8e367c2c31f51242fb77b350a11830F3')
/// Error: invalid checksum address
/// throws Error: invalid checksum address
```
// Sign transactions
wallet.sign({
to: "0x06B5955A67D827CDF91823E3bB8F069e6c89c1D6",
gasLimit: 3000000,
gasPrice: "0x1000",
value: "0x1000"
### Crowdsale JSON Wallets
During the crowdsale, the Ethereum Project sold ether by generating *crowdsale JSON wallet*. These functions allow you to decrypt those files and retreive the private key.
```javascript
// See the test-wallets directory samples (the variable should be a string)
var json = "... see the test-wallets directory for samples ...";
// Detect crowdsale JSON wallets
Wallet.isCrowdsaleWallet(json)
// Get a wallet from a crowdsale JSON wallet
var wallet = Wallet.decryptCrowdsaleWallet(json, password);
console.log(wallet.address)
console.log(wallet.privateKey)
```
### Secret Storage JSON Wallet
This API allows you to decrypt and encrypt the [Secret Storage](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) format used by *Geth* and many other wallet platforms (such as *ethers.io*).
The Secret Storage JSON Wallet format uses an algorithm called *scrypt*, which is intentionally CPU-intensive, which ensures that an attacker would need to tie up considerable resources to attempt to brute-force guess your password. It aslo means it may take some time (10-30 seconds) to decrypt or encrypt a wallet. So, these API calls use a callback to provide progress feedback as well as the opportunity to cancel the process.
The callback should look like `function(progress)` where progress is a Number between 0 and 1 (inclusive) and if the function returns `true`, then the process will be cancelled, calling the callback once more with `callback(new Error('cancelled'))`.
#### Wallet.decrypt(json, password[, callback]);
```javascript
// See the test-wallets directory samples (the variable should be a string)
var json = "... see the test-wallets directory for samples ...";
// Decrypt a Secret Storage JSON wallet
var shouldCancelDecrypt = false;
function updateInterface(progress) {
console.log('The wallet is ' + parseInt(100 * progress) + '% decrypted');
// Optionally return true to stop this decryption; this callback will get
// called once more with callback(new Error("cancelled"))
return shouldCancelDecrypt;
}
Wallet.decrypt(json, password, updateInterface).then(function(wallet) {
// The wallet was successfully decrypted
}, function(error) {
if (error.message === 'invalid password') {
// Wrong password
} else if (error.message === 'cancelled') {
// The decryption was cancelled
}
});
```
#### Wallet.prototype.encrypt(password[, options][, callback]);
```javascript
// Encrypt a wallet into a Secret Storage JSON Wallet (all options are optional)
var bytes16 = '0xdeadbeef1deadbeef2deadbeef301234';
var options = {
salt: bytes16, // hex string or Buffer, any length
iv: bytes16, // hex string or Buffer, 16 bytes
uuid: bytes16, // hex string or Buffer, 16 bytes
scrypt: {
N: (1 << 17), // Number, power of 2 greater than 2
p: 8, // Number
r: 1 // Number
}
}
var wallet = new Wallet(privateKey);
var shouldCancelEncrypt = false;
function updateInterface(progress) {
console.log('The wallet is ' + parseInt(100 * progress) + '% encrypted');
// Optionally return true to stop this encryption; this callback will get
// called once more with callback(new Error("cancelled"))
return shouldCancelEncrypt;
}
wallet.encrypt(password, options, updateInterface).then(function(json) {
// The wallet was successfully encrypted as a json string
}, function(error) {
if (error.message === 'cancelled') {
// Cancelled
}
});
```
### Brain Wallets
Brain wallets should not be considered a secure way to store large amounts of ether; anyone who knows your username/password can steal your funds.
```javascript
// Username and passwords must be buffers; see scrypt-js library for summary
// of UTF-8 gotchas (@TOOD: include a link)
var email = new Wallet.utils.Buffer('github@ricmoo.com', 'utf8');
var password = new Wallet.utils.Buffer('password', 'utf8');
var shouldCancelSummon = false;
function updateInterface(progress) {
console.log('The wallet is ' + parseInt(100 * progress) + '% generated');
// Optionally return true to stop this generation; this callback will get
// called once more with callback(new Error("cancelled"))
return shouldCancelSummon;
}
Wallet.summonBrainWallet(email, password, updateInterface).then(function(wallet) {
// The wallet was successfully generated
}, function(error) {
if (error.message === 'cancelled') {
// Cancelled
}
});
```
Provider API
------------
Connect to standard *Ethereum* nodes via RPC (if you have a local [parity](https://ethcore.io/parity.html) or [geth](https://github.com/ethereum/go-ethereum/wiki/Geth) instance running), or via [Etherscan](https://etherscan.io):
```javascript
// The Web3 library is NOT required, but if you have one (for example from Metamask)
var web3Provider = new Web3.providers.HttpProvider('http://localhost:8545');
var web3 = new Web3(web3Provider);
// All these are equivalent
var wallet = new Wallet(privateKey, 'http://localhost:8545');
var wallet = new Wallet(privateKey, web3Provider);
var wallet = new Wallet(privateKey, web3);
// Or use Etherscan:
var wallet = new Wallet(privateKey, new Wallet.providers.EtherscanProvider({testnet: true}));
// With a provider attached, you can call additional methods on the wallet
// Get the wallet's balance
wallet.getBalance().then(function(balance) {
console.log(balance);
});
// Get the current nonce for this wallet
wallet.getTransactionCount().then(function(transactionCount) {
console.log(transactionCount);
})
// Send ether to another account or contract
wallet.send(targetAddress, Wallet.parseEther('1.0')).then(function(txid) {
console.log(txid);
})
```
Contract API
------------
```javascript
// Load a normal web3 object (you need a local RPC-enabled ethereum node running)
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))
// Create your wallet
var wallet = new Wallet('0x3141592653589793238462643383279502884197169399375105820974944592')
var privateKey = '0x3141592653589793238462643383279502884197169399375105820974944592';
// Create your wallet with any method from the above Provider API
var wallet = new Wallet(privateKey, 'http://localhost:8545')
console.log(wallet.address);
/// "0x7357589f8e367c2C31F51242fB77B350A11830F3"
@@ -103,7 +309,7 @@ var simpleStorageAbi = [
];
// Get the contract
var contract = wallet.getContract(web3, simpleStorageAddress, simpleStorageAbi)
var contract = wallet.getContract(simpleStorageAddress, simpleStorageAbi)
// Set up events
contract.onvaluechanged = function(oldValue, newValue) {
@@ -122,7 +328,7 @@ contract.setValue("Hello World").then(function(txid) {
console.log('txid: ' + txid);
});
// Include ether with a state-changing call, or custom gasLimit or gasPrice
// Include custom parameters with a state-changing call
var options = {
gasPrice: 1000 // in wei (default: from network)
gasLimit: 3000000, // is gas (default: 3000000)
@@ -132,16 +338,53 @@ contract.setValue("Hello World", options).then(function(txid) {
console.log('txid: ' + txid);
});
// Estimate the gas cost of calling a state-changing method (returns a BN.js)
contract.estimate.setValue("Hello World").then(function(gasCost) {
console.log(gasCost.toString(10));
});
```
Testing
-------
A lot of the test cases are performed by comparing against known working implementations of many of the features of this library. To run the test suite, you must use `npm install` (without the `--production` flag, which would skip the development dependencies.)
To run the test suite,
```
/Users/ethers> npm test
> ethers-wallet@0.0.9 test /Users/ethers/ethers-wallet
> nodeunit test.js
Running test cases... (this can take a long time, please be patient)
index.js
+ testPrivateKeyToAddress
+ testChecksumAddress
+ testIcapAddress
+ testEtherFormat
+ testTrasactions
+ testSolidityCoder
+ testContracts
+ testSecretStorage
+ testBrainWallet
+ testContractAddress
+ testProviders
OK: 52178 assertions (147788ms)
```
There are also some test JSON wallets available in the [test-wallets](https://github.com/ethers-io/ethers-wallet/tree/master/test-wallets) directory.
License
-------
MIT Licensed, with the exceptions:
- The Solidity encoder/decoder (LGPL)
- RLP (MPL-2.0)
We are working on our own implementations so we can move off of them and have a completely MIT licensed implementation in the near future.
We are working on our own implementations and will have the library 100% MIT in the near future.
Stay tuned!

22940
dist/ethers-wallet.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,284 @@
<html>
<head>
<title>Ethereum Classic Split Tool</title>
<link rel="stylesheet" type="text/css" href="../style.css">
</head>
<body>
<div class="centerer">
<div class="centered">
<h1>Split Ether Classic</h1>
<hr />
<h2>What does this tool do?</h2>
<p>
This tool will take a <i>geth</i> (or crowdsale) JSON wallet, decrypt it and
send all its funds to <a href="http://etherscan.io/address/0x3474627d4f63a678266bc17171d87f8570936622#code">this contract</a>,
which will:
</p>
<ul>
<li>On the ETH branch &mdash; send the funds back to the original address</li>
<li>On the ETC branch &mdash; send the funds to the provided target address (for example, a <a href="https://www.poloniex.com">Poloniex</a> deposit address)</li>
</ul>
<br />
<h3>Disclaimer:</h3>
<p>
I threw this together in couple of hours, mainly to split my own ether
and test my <i>ethers-wallet</i> library (which is still missing features
and is itself not ready for production use). Testing has been fairly minimal
beyond trying it on a few wallets. <b>Use this at your own risk.</b>
</p>
<hr />
<h2>Check Current ETC Balance</h2>
<table>
<tr>
<th>ETC Address:</th>
<td><input type="text" id="checkAddress" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="submitCheck" class="submit disable">Check Classic Ether Balance</div>
</td>
</tr>
</table>
<hr />
<h2>Split ETC/ETH</h2>
<table>
<tr>
<th>JSON Wallet:</th>
<td><div class="file" id="drop">Drop JSON wallet file here</div><input type="file" id="json" /></td>
</tr>
<tr>
<th>Password:</th>
<td><input type="password" id="password" /></td>
</tr>
<tr>
<th>Target ETC Address:</th>
<td><input type="text" id="targetAddress" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="submitSplit" class="submit disable">Split Classic Ether</div>
</td>
</tr>
</table>
</div>
</div>
<script type="text/javascript" src="../../dist/ethers-wallet.js"></script>
<script type="text/javascript">
var submitCheck = document.getElementById('submitCheck');
var submitSplit = document.getElementById('submitSplit');
var inputJson = document.getElementById('json');
var inputCheckAddress = document.getElementById('checkAddress')
var inputTargetAddress = document.getElementById('targetAddress')
var inputPassword = document.getElementById('password');
var targetDrop = document.getElementById('drop');
var provider = new Wallet.providers.HttpProvider('https://linode-newark.ethers.ws:8002');
var contractAddress = '0x3474627d4f63a678266bc17171d87f8570936622';
var contractAbi = JSON.parse('[{"constant":false,"inputs":[{"name":"balance","type":"uint256"}],"name":"claimDonations","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"isClassic","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"classicAddress","type":"address"}],"name":"split","outputs":[],"type":"function"},{"inputs":[],"type":"constructor"}]');
submitCheck.onclick = function() {
if (submitCheck.classList.contains('disable')) { return; }
var address = document.getElementById('checkAddress').value;
try {
address = Wallet.getAddress(address);
} catch (error) {
console.log(error);
alert('invalid address');
return;
}
provider.getBalance(address, 'latest').then(function(balance) {
alert('Balance: ' + Wallet.formatEther(balance, {commify: true}) + ' ' + Wallet.etherSymbol);
}, function(error) {
if (error) {
console.log(error);
alert('Faied to get balance');
}
});
console.log('check', address);
}
submitSplit.onclick = function() {
if (submitSplit.classList.contains('disable')) { return; }
function done() {
submitSplit.textContent = 'Split Classic Ether';
inputJson.readOnly = false;
inputPassword.readOnly = false;
inputTargetAddress.readOnly = false;
checkSplit();
}
inputJson.readOnly = true;
inputPassword.readOnly = true;
inputTargetAddress.readOnly = true;
submitSplit.classList.add('disable');
var files = inputJson.files;
if (files.length !== 1) {
alert('No wallet found');
return done();
}
var password = new Wallet.utils.Buffer(inputPassword.value, 'utf8');
var targetAddress = document.getElementById('targetAddress').value;
try {
targetAddress = Wallet.getAddress(targetAddress);
} catch (error) {
console.log(error);
alert('invalid target address');
return done();
}
function processWallet(wallet) {
console.log(wallet);
if (wallet.address === targetAddress) {
alert('Wallet address and target address cannot be the same.');
return done();
}
submitSplit.textContent = 'Decrypted \u2014 Processing (please wait)';
var contract = wallet.getContract(contractAddress, contractAbi);
var data = contract.interface.split(targetAddress).data;
var transaction = {
to: contractAddress,
data: data
};
Promise.all([
provider.getBalance(wallet.address, 'latest'),
provider.getTransactionCount(wallet.address, 'latest'),
provider.getGasPrice(),
// provider.estimateGas(transaction)
]).then(function(results) {
var balance = results[0];
var transactionCount = results[1];
var gasPrice = results[2];
var gasEstimate = new Wallet.utils.BN('29735');
//results[3].add(new Wallet.utils.BN(21000));
transaction.gasPrice = '0x' + gasPrice.toString(16);
transaction.nonce = transactionCount;
transaction.gasLimit = gasEstimate;
var toSend = balance.sub(gasPrice.mul(gasEstimate));
transaction.value = toSend;
console.log(transaction);
var accept = confirm('Balance: ' + Wallet.formatEther(balance, {commify: true}) + ' ' + Wallet.etherSymbol + '. Are you sure you want to split Classic Ether to ' + targetAddress + '?');
if (accept) {
var signedTransaction = wallet.sign(transaction);
function showError(error) {
console.log(error);
done();
alert('Error sending transaction');
}
provider.sendTransaction(signedTransaction).then(function(txid) {
if (txid.match(/^0x0+$/)) {
showError(new Error('txid was zero'));
return;
}
done();
console.log('txid:' + txid);
alert('Success \u2014 ' + txid);
}, function(error) {
showError(error);
});
} else {
done();
}
}, function(error) {
done();
console.log(error);
alert('Error: Faied to fetch info');
});
}
var fileReader = new FileReader();
fileReader.onload = function(e) {
var json = e.target.result;
if (Wallet.isCrowdsaleWallet(json)) {
var wallet = Wallet.decryptCrowdsale(json, password);
processWallet(wallet);
} else if (Wallet.isValidWallet(json)) {
Wallet.decrypt(json, password, function(error, wallet, progress) {
if (error) {
done();
console.log(error);
if (error.message === 'invalid password') {
alert('Wrong Password');
} else {
alert('Error Decrypting Wallet');
}
} else if (wallet) {
processWallet(wallet);
} else {
submitSplit.textContent = 'Decrypting \u2014 ' + (parseInt(progress * 100) + '%');
}
});
} else {
alert('unknown walet format');
done();
}
}
fileReader.readAsText(files[0]);
}
inputCheckAddress.oninput = function() {
try {
Wallet.getAddress(inputCheckAddress.value);
submitCheck.classList.remove('disable');
} catch (error) {
submitCheck.classList.add('disable');
}
}
function checkSplit() {
if (!inputJson.files || inputJson.files.length !== 1) {
submitSplit.classList.add('disable');
return;
}
targetDrop.textContent = inputJson.files[0].name;
try {
Wallet.getAddress(inputTargetAddress.value);
} catch (error) {
submitSplit.classList.add('disable');
return;
}
submitSplit.classList.remove('disable');
}
inputTargetAddress.oninput = checkSplit;
inputJson.onchange = checkSplit;
inputJson.addEventListener('dragover', function(event) {
event.preventDefault();
event.stopPropagation();
targetDrop.classList.add('highlight');
}, true);
inputJson.addEventListener('drop', function(event) {
targetDrop.classList.remove('highlight');
}, true);
var check
</script>
</body>
</html>

187
examples/style.css Normal file
View File

@@ -0,0 +1,187 @@
body {
background-color: #eee;
font-family: sans-serif;
font-size: 18px;
margin: 0;
}
.centerer {
margin-left: 50%;
min-height: 100%;
}
.centered:before {
box-shadow: -10px 0 15px -10px #999 inset;
content: " ";
height: 100%;
left: -10px;
position: absolute;
top: 0;
width: 10px;
}
.centered:after {
box-shadow: 10px 0 15px -10px #999 inset;
content: " ";
height: 100%;
right: -10px;
position: absolute;
top: 0;
width: 10px;
}
.centered {
background-color: #fff;
border-left: 1px solid #888;
border-right: 1px solid #888;
dbox-shadow: -1px 0 15px 0 #999;
margin-left: -370px;
min-height: 100%;
padding: 20px;
position: relative;
width: 700px;
}
.hidden {
display: none;
}
p {
text-align: justify;
margin-bottom: 30px;
}
th {
text-align: left;
padding: 0 15px 15px 0;
}
td {
padding: 0 15px 15px 0;
}
input[type=text] {
border: 1px solid #555;
font-size: 16px;
padding: 10px;
width: 500px;
}
input[type=password] {
border: 1px solid #555;
font-size: 16px;
padding: 10px;
width: 500px;
}
input[type=file] {
dbackground: #fff;
border: 1px solid #555;
cursor: pointer;
font-size: 16px;
opacity: 0;
padding: 10px;
position: relative;
dvisibility: hidden;
width: 500px;
}
input[type=text].readonly {
border: 1px solid #ccc;
color: #888;
}
.file {
border: 1px solid green;
color: #444;
font-size: 12px;
line-height: 16px;
margin-top: 2px;
padding: 13px 10px 12px;
position: absolute;
text-align: center;
width: 478px;
}
.file.highlight {
box-shadow: 0px 0px 5px #888;
}
.option {
border: 1px solid #999;
cursor: pointer;
font-size: 16px;
opacity: 0.3;
padding: 10px;
text-align: center;
transition: opacity 0.1s linear;
width: 200px;
}
.option.selected {
opacity: 1;
}
.option:hover {
box-shadow: 0px 0px 5px #888;
opacity: 1;
}
table {
margin-bottom: 40px;
}
div.activity {
font-family: monospace;
margin-bottom: 40px;
}
div.activity a {
display: block;
text-decoration: none;
}
.username {
color: #888;
font-size: 14px;
}
.submit {
border: 1px solid #555;
box-shadow: 0px 0px 5px #888;
cursor: pointer;
font-size: 16px;
padding: 10px;
text-align: center;
transition: opacity 0.1s linear;
width: 480px;
}
.submit:hover {
border: 1px solid #999;
box-shadow: 0px 0px 5px #aaa;
}
.submit:active {
box-shadow: none;
}
.submit.disable {
box-shadow: none;
opacity: 0.5;
}
.submit.disable:hover {
border: 1px solid #555;
box-shadow: none;
}
.submit.disable:active {
box-shadow: none;
}
.left {
float: left;
}
.right {
float: right;
}

500
examples/wallet/index.html Normal file
View File

@@ -0,0 +1,500 @@
<html>
<head>
<title>Ethereum Wallet</title>
<link rel="stylesheet" type="text/css" href="../style.css">
</head>
<body>
<div class="centerer" id="screen-select">
<div class="centered">
<h1>Ethereum Wallet Tool</h1>
<hr />
<h2>Summon Brain Wallet</h2>
<p>
A brain wallet generates an <i>Ethereum</i> wallet from a username and a password
without using any servers to store your information. If you lose your username
or password, <b>no one</b> can help you recover them. Anyone who can guess your
username and password can steal your funds. Brain wallets should <b>not</b> be
considered a safe way to store large amounts of ether nor for long periods of time.
</p>
<table>
<tr>
<th>E-mail Address:</th>
<td><input type="text" placeholder="(e-mail address)" id="select-brainwallet-username" /></td>
</tr>
<tr>
<th>Password:</th>
<td><input type="password" placeholder="(password)" id="select-brainwallet-password" /></td>
</tr>
<tr>
<th>Confirm Password:</th>
<td><input type="password" placeholder="(same password)" id="select-brainwallet-confirm-password" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="select-submit-brainwallet" class="submit disable">Summon Brain Wallet</div>
</td>
</tr>
</table>
<hr />
<h2>Load JSON Wallet</h2>
<p>
If you have a JSON wallet file from <i>geth</i> or from the initial <i>Ethereum</i>
crowd sale, you can decrypt it here. No information is shared with <b>any</b>
server.
</p>
<table>
<tr>
<th>JSON Wallet:</th>
<td><div class="file" id="select-wallet-drop">Drop JSON wallet file here</div><input type="file" id="select-wallet-file" /></td>
</tr>
<tr>
<th>Password:</th>
<td><input type="password" placeholder="(password)" id="select-wallet-password" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="select-submit-wallet" class="submit disable">Unlock JSON Wallet</div>
</td>
</tr>
</table>
<hr />
<h2>Raw Private Key</h2>
<p>
</p>
<table>
<tr>
<th>Private Key:</th>
<td><input type="text" placeholder="(private key)" id="select-privatekey" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="select-submit-privatekey" class="submit disable">Unlock JSON Wallet</div>
</td>
</tr>
</table>
<hr />
<h3>Disclaimer:</h3>
<p>
This is beta software, with no warranty. <b>Use at your own risk.</b>
</p>
</div>
</div>
<div class="centerer hidden" id="screen-loading">
<div class="centered">
<h1>Loading Wallet</h1>
<hr />
<h2 id="loading-header"></h2>
<table>
<tr>
<th>Progress:</th>
<td>
<input type="text" readonly="readonly" class="readonly" id="loading-status" value="" /></div>
</td>
</tr>
<tr>
<td> </td>
<td>
<div id="loading-cancel" class="submit">Cancel</div>
</td>
</tr>
</table>
<hr />
<h3>Disclaimer:</h3>
<p>
This is beta software, with no warranty. <b>Use at your own risk.</b>
</p>
</div>
</div>
<div class="centerer hidden" id="screen-wallet">
<div class="centered">
<h1>Ethereum Wallet<span id="wallet-username" class="username right"></span></h1>
<hr />
<h3>Wallet Details:</h3>
<table>
<tr>
<th>Address:</th>
<td>
<input type="text" readonly="readonly" class="readonly" id="wallet-address" value="" /></div>
</td>
</tr>
<tr>
<th>Network:</th>
<td>
<div class="option left" id="option-morden">Morden (testnet)</div>
<div class="option right selected" id="option-homestead">Homestead (mainnet)</div>
</td>
</tr>
<tr>
<th>Balance:</th>
<td>
<input type="text" readonly="readonly" class="readonly" id="wallet-balance" value="0.0" /></div>
</td>
</tr>
<tr>
<th>Nonce:</th>
<td>
<input type="text" readonly="readonly" class="readonly" id="wallet-transaction-count" value="0" /></div>
</td>
</tr>
<tr>
<td> </td>
<td>
<div id="wallet-submit-refresh" class="submit">Refresh</div>
</td>
</tr>
</table>
<h3>Send Ether:</h3>
<table>
<tr>
<th>Target Address:</th>
<td><input type="text" placeholder="(target address)" id="wallet-send-target-address" /></td>
</tr>
<tr>
<th>Amount:</th>
<td><input type="text" placeholder="(amount)" id="wallet-send-amount" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="wallet-submit-send" class="submit disable">Send Ether</div>
</td>
</tr>
</table>
<h3>Session Activity</h3>
<div id="wallet-activity" class="activity"></div>
<hr />
<h3>Disclaimer:</h3>
<p>
This is beta software, with no warranty. <b>Use at your own risk.</b>
</p>
</div>
</div>
<script type="text/javascript" src="../../dist/ethers-wallet.js"></script>
<script type="text/javascript">
function setEnter(source, target) {
source.onkeyup = function(e) {
if (e.which === 13) { target.click(); }
}
}
var cancelScrypt = false;
document.getElementById('loading-cancel').onclick = function() {
cancelScrypt = true;
};
(function() {
var inputUsername = document.getElementById('select-brainwallet-username');
var inputPassword = document.getElementById('select-brainwallet-password');
var inputConfirmPassword = document.getElementById('select-brainwallet-confirm-password');
var submit = document.getElementById('select-submit-brainwallet');
function checkBrainwallet() {
if (inputUsername.value && inputPassword.value && inputPassword.value === inputConfirmPassword.value) {
submit.classList.remove('disable');
} else {
submit.classList.add('disable');
}
}
inputUsername.oninput = checkBrainwallet;
inputPassword.oninput = checkBrainwallet;
inputConfirmPassword.oninput = checkBrainwallet;
setEnter(inputUsername, submit);
setEnter(inputPassword, submit);
setEnter(inputConfirmPassword, submit);
submit.onclick = function() {
if (submit.classList.contains('disable')) { return; }
var username = new Wallet.utils.Buffer(inputUsername.value, 'utf8');
var password = new Wallet.utils.Buffer(inputPassword.value, 'utf8');
showLoading('Summoning Brain Wallet...');
cancelScrypt = false;
Wallet.summonBrainWallet(username, password, function(error, wallet, progress) {
if (error) {
if (error.message !== 'cancelled') {
alert('Unknown error');
}
showSelect();
} else if (wallet) {
showWallet(wallet);
document.getElementById('wallet-username').textContent = inputUsername.value;
} else {
updateLoading(progress);
}
return cancelScrypt;
});
};
})();
(function() {
var inputFile = document.getElementById('select-wallet-file');
var targetDrop = document.getElementById('select-wallet-drop');
var inputPassword = document.getElementById('select-wallet-password');
var submit = document.getElementById('select-submit-wallet');
function check() {
if (inputFile.files && inputFile.files.length === 1) {
submit.classList.remove('disable');
targetDrop.textContent = inputFile.files[0].name;
} else {
submit.classList.add('disable');
}
}
inputFile.onchange = check;
inputPassword.oninput = check;
setEnter(inputPassword, submit);
inputFile.addEventListener('dragover', function(event) {
event.preventDefault();
event.stopPropagation();
targetDrop.classList.add('highlight');
}, true);
inputFile.addEventListener('drop', function(event) {
targetDrop.classList.remove('highlight');
}, true);
submit.onclick = function() {
if (submit.classList.contains('disable')) { return; }
var fileReader = new FileReader();
fileReader.onload = function(e) {
var json = e.target.result;
if (Wallet.isCrowdsaleWallet(json)) {
showWallet(Wallet.decryptCrowdsale(json, password));
} else if (Wallet.isValidWallet(json)) {
showLoading('Decrypting Wallet...');
var password = new Wallet.utils.Buffer(inputPassword.value);
cancelScrypt = false;
Wallet.decrypt(json, password, function(error, wallet, progress) {
if (error) {
if (error.message === 'invalid password') {
alert('Wrong Password');
} else {
console.log(error);
alert('Error Decrypting Wallet');
}
showSelect();
} else if (wallet) {
showWallet(wallet);
} else {
updateLoading(progress);
}
return cancelScrypt;
});
} else {
alert('Unknown JSON wallet format');
}
};
fileReader.readAsText(inputFile.files[0]);
};
})();
(function() {
var inputPrivatekey = document.getElementById('select-privatekey');
var submit = document.getElementById('select-submit-privatekey');
function check() {
if (inputPrivatekey.value.match(/^(0x)?[0-9A-fa-f]{64}$/)) {
submit.classList.remove('disable');
} else {
submit.classList.add('disable');
}
}
inputPrivatekey.oninput = check;
setEnter(inputPrivatekey, submit);
submit.onclick = function() {
if (submit.classList.contains('disable')) { return; }
var privateKey = inputPrivatekey.value;
if (privateKey.substring(0, 2) !== '0x') { privateKey = '0x' + privateKey; }
showWallet(new Wallet(privateKey));
}
})();
var activeWallet = null;
function showError(error) {
alert('Error \u2014 ' + error.message);
}
// Refresh balance and transaction count in the UI
var refresh = (function() {
var inputBalance = document.getElementById('wallet-balance');
var inputTransactionCount = document.getElementById('wallet-transaction-count');
var submit = document.getElementById('wallet-submit-refresh');
function refresh() {
addActivity('> Refreshing details...');
activeWallet.getBalance('pending').then(function(balance) {
addActivity('< Balance: ' + balance.toString(10));
inputBalance.value = Wallet.formatEther(balance, {commify: true});
}, function(error) {
showError(error);
});
activeWallet.getTransactionCount('pending').then(function(transactionCount) {
addActivity('< TransactionCount: ' + transactionCount);
inputTransactionCount.value = transactionCount;
}, function(error) {
showError(error);
});
}
submit.onclick = refresh;
return refresh;
})();
var addActivity = (function() {
var activity = document.getElementById('wallet-activity');
return function(message, url) {
var line = document.createElement('a');
line.textContent = message;
if (url) { line.setAttribute('href', url); }
activity.appendChild(line);
}
})();
// Set up the wallet page
(function() {
var inputTargetAddress = document.getElementById('wallet-send-target-address');
var inputAmount = document.getElementById('wallet-send-amount');
var submit = document.getElementById('wallet-submit-send');
// Validate the address and value (to enable the send button)
function check() {
try {
Wallet.getAddress(inputTargetAddress.value);
Wallet.parseEther(inputAmount.value);
} catch (error) {
submit.classList.add('disable');
return;
}
submit.classList.remove('disable');
}
inputTargetAddress.oninput = check;
inputAmount.oninput = check;
var optionMorden = document.getElementById('option-morden');
var optionHomestead = document.getElementById('option-homestead');
// Select the morden network
optionMorden.onclick = function() {
if (optionMorden.classList.contains('selected')) { return; }
addActivity('! Switched network: Morden');
activeWallet.provider = new Wallet.providers.EtherscanProvider({testnet: true});
optionMorden.classList.add('selected');
optionHomestead.classList.remove('selected');
refresh();
}
// Select the homestead network
optionHomestead.onclick = function() {
if (optionHomestead.classList.contains('selected')) { return; }
addActivity('! Switched network: Homestead');
activeWallet.provider = new Wallet.providers.EtherscanProvider({testnet: false});
optionMorden.classList.remove('selected');
optionHomestead.classList.add('selected');
refresh();
}
// Send ether
submit.onclick = function() {
// Matt (from Etherscan) is working on a gasPrice API call, which
// should be done within a week or so.
var gasPrice = (activeWallet.provider.testnet ? 0x4a817c800: 0xba43b7400);
console.log('GasPrice: ' + gasPrice);
var targetAddress = Wallet.getAddress(inputTargetAddress.value);
var amountWei = Wallet.parseEther(inputAmount.value);
activeWallet.send(targetAddress, amountWei, {
gasPrice: gasPrice,
gasLimit: 21000,
}).then(function(txid) {
var url = (activeWallet.provider.testnet ? 'https://testnet.etherscan.io/tx/': 'https://etherscan.io/tx/') + txid;
addActivity('< Transaction sent: ' + txid.substring(0, 20) + '...', url);
alert('Success!');
inputTargetAddress.value = '';
inputAmount.value = '';
submit.classList.add('disable');
refresh();
}, function(error) {
showError(error);
});
}
})();
function showSelect() {
document.getElementById('screen-select').style.display = 'block';
document.getElementById('screen-loading').style.display = 'none';
document.getElementById('screen-wallet').style.display = 'none';
}
function showLoading(title) {
document.getElementById('screen-select').style.display = 'none';
document.getElementById('screen-loading').style.display = 'block';
document.getElementById('screen-wallet').style.display = 'none';
document.getElementById('loading-header').textContent = title;
}
var loadingStatus = document.getElementById('loading-status');
function updateLoading(progress) {
loadingStatus.value = (parseInt(progress * 100)) + '%';
}
function showWallet(wallet) {
var testnet = document.getElementById('option-morden').classList.contains('selected');
activeWallet = wallet;
activeWallet.provider = new Wallet.providers.EtherscanProvider({testnet: testnet});
document.getElementById('screen-select').style.display = 'none';
document.getElementById('screen-loading').style.display = 'none';
document.getElementById('screen-wallet').style.display = 'block';
var inputWalletAddress = document.getElementById('wallet-address');
inputWalletAddress.value = wallet.address;
inputWalletAddress.onclick = function() {
this.select();
};
refresh();
}
//var privateKey = '0x3141592653589793238462643383279502884197169399375105820974944592';
//showWallet(new Wallet(privateKey));
</script>
</body>
</html>

476
index.js
View File

@@ -1,408 +1,96 @@
var aes = require('aes-js');
var elliptic = require('elliptic');
//var elliptic = require('./elliptic/lib/elliptic.js');
var pbkdf2 = require('pbkdf2');
var rlp = require('rlp');
'use strict';
var scrypt = require('scrypt-js');
var uuid = require('uuid');
var Contract = require('./lib/contract.js');
var providers = require('./lib/providers.js');
var secretStorage = require('./lib/secret-storage.js');
var Randomish = require('./lib/randomish.js');
var SigningKey = require('./lib/signing-key.js');
var Wallet = require('./lib/wallet.js');
var units = require('./lib/units.js');
var utils = require('./lib/utils.js');
var BN = utils.BN;
var secp256k1 = new (elliptic.ec)('secp256k1');
/*
// @TODO: Make our own implementation of rlp; the existing one is MLP-2.0, we want MIT. :)
function rlpEncodeLength(length) {
}
function rlpArray(items) {
var output = new Buffer(0);
for (var i = 0; i < items.length; i++) {
}
}
*/
function defineProperty(object, name, value) {
Object.defineProperty(object, name, {
enumerable: true,
value: value
});
}
var exportUtils = {};
utils.defineProperty(Wallet, 'utils', exportUtils);
// These may go away in the future...
defineProperty(exportUtils, '_aes', aes);
defineProperty(exportUtils, '_scrypt', scrypt);
utils.defineProperty(exportUtils, 'BN', BN);
utils.defineProperty(exportUtils, 'Buffer', Buffer);
defineProperty(exportUtils, 'BN', BN);
defineProperty(exportUtils, 'Buffer', Buffer);
defineProperty(exportUtils, 'sha3', utils.sha3);
utils.defineProperty(exportUtils, 'sha3', utils.sha3);
utils.defineProperty(exportUtils, 'sha256', utils.sha256);
function getChecksumAddress(address) {
if (typeof(address) !== 'string' || !address.match(/^0x[0-9A-Fa-f]{40}$/)) {
throw new Error('invalid address');
}
address = address.substring(2).toLowerCase();
var hash = utils.sha3(address);
address = address.split('');
for (var i = 0; i < 40; i += 2) {
if ((hash[i >> 1] >> 4) >= 8) {
address[i] = address[i].toUpperCase();
}
if ((hash[i >> 1] & 0x0f) >= 8) {
address[i + 1] = address[i + 1].toUpperCase();
}
}
return '0x' + address.join('');
}
// 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(Math.log10(Number.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);
}
checksum = String(98 - (parseInt(expanded, 10) % 97));
while (checksum.length < 2) { checksum = '0' + checksum; }
return checksum;
};
})();
// http://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed
defineProperty(exportUtils, 'getContractAddress', function(transaction) {
if (typeof(address) !== 'string' || !address.match(/^0x[0-9A-Fa-f]{40}$/)) {
throw new Error('invalid from');
}
return '0x' + utils.sha3(rlp.encode([
utils.hexOrBuffer(transaction.from),
utils.hexOrBuffer(utils.hexlify(transaction.nonce, 'nonce'))
])).slice(12).toString('hex');
});
var transactionFields = [
{name: 'nonce', maxLength: 32, },
{name: 'gasPrice', maxLength: 32, },
{name: 'gasLimit', maxLength: 32, },
{name: 'to', length: 20, },
{name: 'value', maxLength: 32, },
{name: 'data'},
];
function Wallet(privateKey) {
if (!(this instanceof Wallet)) { throw new Error('missing new'); }
if (typeof(privateKey) === 'string' && privateKey.match(/^0x[0-9A-Fa-f]{64}$/)) {
privateKey = new Buffer(privateKey.substring(2), 'hex');
} else if (!Buffer.isBuffer(privateKey) || privateKey.length !== 32) {
throw new Error('invalid private key');
}
var keyPair = secp256k1.keyFromPrivate(privateKey);
var publicKey = (new Buffer(keyPair.getPublic(false, 'hex'), 'hex')).slice(1);
var address = Wallet.getAddress(utils.sha3(publicKey).slice(12).toString('hex'));
defineProperty(this, 'address', address);
defineProperty(this, 'sign', function(transaction) {
var raw = [];
transactionFields.forEach(function(fieldInfo) {
var value = transaction[fieldInfo.name] || (new Buffer(0));
value = utils.hexOrBuffer(utils.hexlify(value), fieldInfo.name);
// Fixed-width field
if (fieldInfo.length && value.length !== fieldInfo.length) {
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(value);
});
var digest = utils.sha3(rlp.encode(raw));
var signature = keyPair.sign(digest, {canonical: true});
var s = signature.s;
var v = signature.recoveryParam;
raw.push(new Buffer([27 + v]));
raw.push(utils.bnToBuffer(signature.r));
raw.push(utils.bnToBuffer(s));
return ('0x' + rlp.encode(raw).toString('hex'));
});
}
defineProperty(Wallet, 'utils', exportUtils);
defineProperty(Wallet, 'getAddress', function(address) {
var result = null;
if (typeof(address) !== 'string') { throw new Error('invalid 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) {
throw new Error('invalid address checksum');
}
// 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)) {
throw new Error('invalid address icap checksum');
}
result = (new BN(address.substring(4), 36)).toString(16);
while (result.length < 40) { result = '0' + result; }
result = getChecksumAddress('0x' + result);
} else {
throw new Error('invalid address');
}
return result;
});
defineProperty(Wallet, 'getIcapAddress', function(address) {
address = Wallet.getAddress(address).substring(2);
var base36 = (new BN(address, 16)).toString(36).toUpperCase();
while (base36.length < 30) { base36 = '0' + base36; }
return 'XE' + ibanChecksum('XE00' + base36) + base36;
});
defineProperty(Wallet, '_Contract', Contract);
var allowedTransactionKeys = {
data: true, from: true, gasLimit: true, gasPrice:true, to: true, value: true
}
function AttachedContract(web3, wallet, contractAddress, contract) {
utils.defineProperty(this, 'web3', web3);
utils.defineProperty(this, 'wallet', wallet);
utils.defineProperty(this, 'contractAddress', contractAddress);
utils.defineProperty(this, '_contract', contract);
function getWeb3Promise(method) {
var params = Array.prototype.slice.call(arguments, 1);
return new Promise(function(resolve, reject) {
params.push(function(error, result) {
if (error) {
return reject(error);
}
resolve(result);
});
web3.eth[method].apply(web3, params);
});
}
var self = this;
var filters = {};
function setupFilter(call, callback) {
var info = filters[call.name];
// Stop and remove the filter
if (!callback) {
if (info) { info.filter.stopWatching(); }
delete filters[call.name];
return;
}
if (typeof(callback) !== 'function') {
throw new Error('invalid callback');
}
// Already have a filter, just update the callback
if (info) {
info.callback = callback;
return;
}
info = {callback: callback};
filters[call.name] = info;
// Start a new filter
info.filter = web3.eth.filter({
address: contractAddress,
topics: call.topics
}, function(error, result) {
// @TODO: Emit errors to .onerror? Maybe?
if (error) {
console.log(error);
return;
}
try {
info.callback.apply(self, call.parse(result.data));
} catch(error) {
console.log(error);
}
});
}
function runMethod(method) {
return function() {
var transaction = {}
var params = Array.prototype.slice.call(arguments);
if (params.length == contract[method].inputs.length + 1) {
transaction = params.pop();
if (typeof(transaction) !== 'object') {
throw new Error('invalid transaction overrides');
}
for (var key in transaction) {
if (!allowedTransactionKeys[key]) {
throw new Error('unknown transaction override ' + key);
}
}
}
var call = contract[method].apply(contract, params);
switch (call.type) {
case 'call':
['data', 'gasLimit', 'gasPrice', 'to', 'value'].forEach(function(key) {
if (transaction[key] != null) {
throw new Error('call cannot override ' + key) ;
}
});
transaction.data = call.data;
if (transaction.from == null) {
transaction.from = wallet.address;
}
transaction.to = contractAddress;
return new Promise(function(resolve, reject) {
getWeb3Promise('call', transaction).then(function(value) {
resolve(call.parse(value));
}, function(error) {
reject(error);
});
});
case 'transaction':
['data', 'from', 'to'].forEach(function(key) {
if (transaction[key] != null) {
throw new Error('transaction cannot override ' + key) ;
}
});
transaction.data = call.data;
transaction.to = contractAddress;
if (transaction.gasLimit == null) {
transaction.gasLimit = 3000000;
}
return new Promise(function(resolve, reject) {
Promise.all([
getWeb3Promise('getTransactionCount', wallet.address, 'pending'),
getWeb3Promise('getGasPrice'),
]).then(function(results) {
if (transaction.nonce == null) {
transaction.nonce = results[0];
} else if (console.warn) {
console.warn('Overriding suggested nonce: ' + results[0]);
}
if (transaction.gasPrice == null) {
transaction.gasPrice = results[1];
} else if (console.warn) {
console.warn('Overriding suggested gasPrice: ' + utils.hexlify(results[1]));
}
var signedTransaction = wallet.sign(transaction);
/*
data: call.data,
gasLimit: 3000000,
gasPrice: results[1],
nonce: results[0],
to: contractAddress,
});
*/
getWeb3Promise('sendRawTransaction', signedTransaction).then(function(txid) {
resolve(txid);
}, function(error) {
reject(error);
});
}, function(error) {
reject(error);
});
});
}
};
}
contract.methods.forEach(function(method) {
utils.defineProperty(this, method, runMethod(method));
}, this);
contract.events.forEach(function(method) {
var call = contract[method].apply(contract, []);
Object.defineProperty(self, 'on' + call.name.toLowerCase(), {
enumerable: true,
get: function() {
console.log('get');
var info = filters[call.name];
if (!info || !info[call.name]) { return null; }
return info.callback;
},
set: function(value) {
console.log('set');
setupFilter(call, value);
}
});
}, this);
}
defineProperty(Wallet.prototype, 'getContract', function(web3, address, abi) {
return new AttachedContract(web3, this, address, new Contract(abi));
});
utils.defineProperty(exportUtils, 'getContractAddress', utils.getContractAddress);
module.exports = Wallet;
utils.defineProperty(Wallet, 'etherSymbol', '\uD835\uDF63');
utils.defineProperty(Wallet, 'formatEther', units.formatEther);
utils.defineProperty(Wallet, 'parseEther', units.parseEther);
utils.defineProperty(Wallet, 'getAddress', utils.getAddress);
utils.defineProperty(Wallet, 'getIcapAddress', utils.getIcapAddress);
utils.defineProperty(Wallet, 'isCrowdsaleWallet', secretStorage.isCrowdsaleWallet);
utils.defineProperty(Wallet, 'isValidWallet', secretStorage.isValidWallet);
utils.defineProperty(Wallet, 'decryptCrowdsale', function(json, password) {
return new Wallet(secretStorage.decryptCrowdsale(json, password));
});
utils.defineProperty(Wallet, 'decrypt', function(json, password, progressCallback) {
if (progressCallback && typeof(progressCallback) !== 'function') {
throw new Error('invalid callback');
}
return new Promise(function(resolve, reject) {
secretStorage.decrypt(json, password, progressCallback).then(function(signingKey) {
resolve(new Wallet(signingKey));
}, function(error) {
reject(error);
});
});
});
utils.defineProperty(Wallet.prototype, 'encrypt', function(password, options, progressCallback) {
if (typeof(options) === 'function' && !progressCallback) {
progressCallback = options;
options = {};
}
if (progressCallback && typeof(progressCallback) !== 'function') {
throw new Error('invalid callback');
}
return secretStorage.encrypt(this.privateKey, password, options, progressCallback);
});
utils.defineProperty(Wallet, 'summonBrainWallet', function(username, password, progressCallback) {
if (progressCallback && typeof(progressCallback) !== 'function') {
throw new Error('invalid callback');
}
return new Promise(function(resolve, reject) {
scrypt(password, username, (1 << 18), 8, 1, 32, function(error, progress, key) {
if (error) {
reject(error);
} else if (key) {
resolve(new Wallet(new Buffer(key)));
} else if (progressCallback) {
progressCallback(progress);
}
});
});
});
utils.defineProperty(Wallet, 'providers', providers);
utils.defineProperty(Wallet, 'randomish', new Randomish());
module.exports = Wallet;

View File

@@ -0,0 +1,37 @@
'use strict';
var utils = require('./utils.js');
var crypto = global.crypto || global.msCrypto;
if (!crypto || !crypto.getRandomValues) {
console.log('WARNING: Missing strong random number source; using weak randomBytes');
crypto = {
getRandomValues: function(length) {
for (var i = 0; i < buffer.length; i++) {
buffer[i] = parseInt(256 * Math.random());
}
return buffer;
},
_weakCrypto: true
};
} else {
console.log('Found strong random number source');
}
function randomBytes(length) {
if (length <= 0 || length > 1024 || parseInt(length) != length) {
throw new Error('invalid length');
}
var buffer = new Buffer(length);
crypto.getRandomValues(buffer);
return buffer;
};
if (crypto._weakCrypto === true) {
utils.defineProperty(randomBytes, '_weakCrypto', true);
}
module.exports = randomBytes;

View File

@@ -0,0 +1,8 @@
'use strict';
try {
module.exports.XMLHttpRequest = XMLHttpRequest;
} catch(error) {
console.log('Warning: XMLHttpRequest is not defined');
module.exports.XMLHttpRequest = null;
}

View File

@@ -1,8 +1,8 @@
// "coder" is not MIT... We need to find/make an MIT compatible implementation
var coder = require('../node_modules/web3/lib/solidity/coder.js');
'use strict';
var utils = require('./utils.js');
// Creates property that is immutable
function defineFrozen(object, name, value) {
var frozen = JSON.stringify(value);
Object.defineProperty(object, name, {
@@ -11,6 +11,7 @@ function defineFrozen(object, name, value) {
});
}
// getKeys([{a: 1, b: 2}, {a: 3, b: 4}], 'a') => [1, 3]
function getKeys(params, key) {
if (!Array.isArray(params)) { throw new Error('invalid params'); }
@@ -18,15 +19,292 @@ function getKeys(params, key) {
for (var i = 0; i < params.length; i++) {
if (typeof(params[i][key]) !== 'string') { throw new Error('invalid abi'); }
// @TODO: Check type are valid?
result.push(params[i][key]);
}
return result;
}
function Contract(abi) {
if (!(this instanceof Contract)) { throw new Error('missing new'); }
// Convert the value from a Number to a BN (if necessary)
function numberOrBN(value) {
if (!value.eq) {
if (typeof(value) !== 'number') {
throw new Error('invalid number');
}
value = new utils.BN(value);
}
return value;
}
function zpad(buffer, length) {
var zero = new Buffer([0]);
while (buffer.length < length) {
buffer = Buffer.concat([zero, buffer]);
}
return buffer;
}
// There seems to be a but in maskn, so we are doing this for now.
var bitmasks = [];
(function() {
var mask = '';
for (var i = 0; i < 33; i++) {
bitmasks.push(new utils.BN(mask, 16));
mask += 'ff';
}
})();
function coderNumber(size, signed) {
return {
encode: function(value) {
value = numberOrBN(value)
value = value.toTwos(size * 8).and(bitmasks[size]);
if (signed) {
value = value.fromTwos(size * 8).toTwos(256);
}
return value.toArrayLike(Buffer, 'be', 32);
},
decode: function(data, offset) {
var junkLength = 32 - size;
var value = new utils.BN(data.slice(offset + junkLength, offset + 32));
if (signed) {
value = value.fromTwos(size * 8);
} else {
value = value.and(bitmasks[size]);
}
return {
consumed: 32,
value: value,
}
}
};
}
var uint256Coder = coderNumber(32, false);
var coderBoolean = {
encode: function(value) {
return uint256Coder.encode(value ? 1: 0);
},
decode: function(data, offset) {
var result = uint256Coder.decode(data, offset);
return {
consumed: result.consumed,
value: !result.value.isZero()
}
}
}
function coderFixedBytes(length) {
return {
encode: function(value) {
value = utils.hexOrBuffer(value);
if (length === 32) { return value; }
var result = new Buffer(32);
result.fill(0);
value.copy(result);
return result;
},
decode: function(data, offset) {
if (data.length < offset + 32) { throw new Error('invalid bytes' + length); }
return {
consumed: 32,
value: '0x' + data.slice(offset, offset + length).toString('hex')
}
}
};
}
var coderAddress = {
encode: function(value) {
if (!utils.isHexString(value, 20)) { throw new Error('invalid address'); }
value = utils.hexOrBuffer(value);
var result = new Buffer(32);
result.fill(0);
value.copy(result, 12);
return result;
},
decode: function(data, offset) {
if (data.length < offset + 32) { throw new Error('invalid address'); }
return {
consumed: 32,
value: '0x' + data.slice(offset + 12, offset + 32).toString('hex')
}
}
}
function _encodeDynamicBytes(value) {
var dataLength = parseInt(32 * Math.ceil(value.length / 32));
var padding = new Buffer(dataLength - value.length);
padding.fill(0);
return Buffer.concat([
uint256Coder.encode(value.length),
value,
padding
]);
}
function _decodeDynamicBytes(data, offset) {
if (data.length < offset + 32) { throw new Error('invalid bytes'); }
var length = uint256Coder.decode(data, offset).value;
length = length.toNumber();
if (data.length < offset + 32 + length) { throw new Error('invalid bytes'); }
return {
consumed: parseInt(32 + 32 * Math.ceil(length / 32)),
value: data.slice(offset + 32, offset + 32 + length),
}
}
var coderDynamicBytes = {
encode: function(value) {
return _encodeDynamicBytes(utils.hexOrBuffer(value));
},
decode: function(data, offset) {
var result = _decodeDynamicBytes(data, offset);
result.value = '0x' + result.value.toString('hex');
return result;
},
dynamic: true
};
var coderString = {
encode: function(value) {
return _encodeDynamicBytes(new Buffer(value, 'utf8'));
},
decode: function(data, offset) {
var result = _decodeDynamicBytes(data, offset);
result.value = result.value.toString('utf8');
return result;
},
dynamic: true
};
function coderArray(coder, length) {
return {
encode: function(value) {
if (!Array.isArray(value)) { throw new Error('invalid array'); }
var result = new Buffer(0);
if (length === -1) {
length = value.length;
result = uint256Coder.encode(length);
}
if (length !== value.length) { throw new Error('size mismatch'); }
value.forEach(function(value) {
result = Buffer.concat([
result,
coder.encode(value)
]);
});
return result;
},
decode: function(data, offset) {
// @TODO:
//if (data.length < offset + length * 32) { throw new Error('invalid array'); }
var consumed = 0;
var result;
if (length === -1) {
result = uint256Coder.decode(data, offset);
length = result.value.toNumber();
consumed += result.consumed;
offset += result.consumed;
}
var value = [];
for (var i = 0; i < length; i++) {
var result = coder.decode(data, offset);
consumed += result.consumed;
offset += result.consumed;
value.push(result.value);
}
return {
consumed: consumed,
value: value,
}
},
dynamic: (length === -1)
}
}
// Break the type up into [staticType][staticArray]*[dynamicArray]? | [dynamicType] and
// build the coder up from its parts
var paramTypePart = new RegExp(/^((u?int|bytes)([0-9]*)|(address|bool|string)|(\[([0-9]*)\]))/);
function getParamCoder(type) {
var coder = null;
while (type) {
var part = type.match(paramTypePart);
if (!part) { throw new Error('invalid type: ' + type); }
type = type.substring(part[0].length);
var prefix = (part[2] || part[4] || part[5]);
switch (prefix) {
case 'int': case 'uint':
if (coder) { throw new Error('invalid type ' + type); }
var size = parseInt(part[3] || 256);
if (size === 0 || size > 256 || (size % 8) !== 0) {
throw new Error('invalid type ' + type);
}
coder = coderNumber(size / 8, (prefix === 'int'));
break;
case 'bool':
if (coder) { throw new Error('invalid type ' + type); }
coder = coderBoolean;
break;
case 'string':
if (coder) { throw new Error('invalid type ' + type); }
coder = coderString;
break;
case 'bytes':
if (coder) { throw new Error('invalid type ' + type); }
if (part[3]) {
var size = parseInt(part[3]);
if (size === 0 || size > 32) {
throw new Error('invalid type ' + type);
}
coder = coderFixedBytes(size);
} else {
coder = coderDynamicBytes;
}
break;
case 'address':
if (coder) { throw new Error('invalid type ' + type); }
coder = coderAddress;
break;
case '[]':
if (!coder || coder.dynamic) { throw new Error('invalid type ' + type); }
coder = coderArray(coder, -1);
break;
// "[0-9+]"
default:
if (!coder || coder.dynamic) { throw new Error('invalid type ' + type); }
var size = parseInt(part[6]);
coder = coderArray(coder, size);
}
}
if (!coder) { throw new Error('invalid type'); }
return coder;
}
function Interface(abi) {
if (!(this instanceof Interface)) { throw new Error('missing new'); }
//defineProperty(this, 'address', address);
@@ -36,6 +314,8 @@ function Contract(abi) {
var methods = [], events = [];
abi.forEach(function(method) {
var func = null;
switch (method.type) {
case 'function':
methods.push(method.name);
@@ -60,14 +340,14 @@ function Contract(abi) {
signature = '0x' + utils.sha3(signature).slice(0, 4).toString('hex');
result.data = signature + coder.encodeParams(inputTypes, params);
result.data = signature + Interface.encodeParams(inputTypes, params).substring(2);
if (method.constant) {
result.type = 'call';
result.parse = function(data) {
return coder.decodeParams(
return Interface.decodeParams(
outputTypes,
utils.hexOrBuffer(data).toString('hex')
)
utils.hexOrBuffer(data)
);
};
} else {
result.type = 'transaction';
@@ -97,9 +377,9 @@ function Contract(abi) {
topics: ['0x' + utils.sha3(signature).toString('hex')],
};
result.parse = function(data) {
return coder.decodeParams(
return Interface.decodeParams(
inputTypes,
utils.hexOrBuffer(data).toString('hex')
utils.hexOrBuffer(data)
);
};
return result;
@@ -117,6 +397,7 @@ function Contract(abi) {
})();
break;
}
utils.defineProperty(this, method.name, func);
}, this);
@@ -124,4 +405,254 @@ function Contract(abi) {
defineFrozen(this, 'events', events);
}
utils.defineProperty(Interface, 'encodeParams', function(types, values) {
if (types.length !== values.length) { throw new Error('types/values mismatch'); }
var parts = [];
types.forEach(function(type, index) {
var coder = getParamCoder(type);
parts.push({dynamic: coder.dynamic, value: coder.encode(values[index])});
})
function alignSize(size) {
return parseInt(32 * Math.ceil(size / 32));
}
var staticSize = 0, dynamicSize = 0;
parts.forEach(function(part) {
if (part.dynamic) {
staticSize += 32;
dynamicSize += alignSize(part.value.length);
} else {
staticSize += alignSize(part.value.length);
}
});
var offset = 0, dynamicOffset = staticSize;
var data = new Buffer(staticSize + dynamicSize);
parts.forEach(function(part, index) {
if (part.dynamic) {
uint256Coder.encode(dynamicOffset).copy(data, offset);
offset += 32;
part.value.copy(data, dynamicOffset);
dynamicOffset += alignSize(part.value.length);
} else {
part.value.copy(data, offset);
offset += alignSize(part.value.length);
}
});
return '0x' + data.toString('hex');
});
utils.defineProperty(Interface, 'decodeParams', function(types, data) {
data = utils.hexOrBuffer(data);
var values = [];
var offset = 0;
types.forEach(function(type) {
var coder = getParamCoder(type);
if (coder.dynamic) {
var dynamicOffset = uint256Coder.decode(data, offset);
var result = coder.decode(data, dynamicOffset.value.toNumber());
offset += dynamicOffset.consumed;
} else {
var result = coder.decode(data, offset);
offset += result.consumed;
}
values.push(result.value);
});
return values;
});
var allowedTransactionKeys = {
data: true, from: true, gasLimit: true, gasPrice:true, to: true, value: true
}
function Contract(wallet, contractAddress, contractInterface) {
utils.defineProperty(this, 'wallet', wallet);
utils.defineProperty(this, 'contractAddress', contractAddress);
utils.defineProperty(this, 'interface', contractInterface);
var self = this;
var filters = {};
function setupFilter(call, callback) {
var info = filters[call.name];
// Stop and remove the filter
if (!callback) {
if (info) { info.filter.stopWatching(); }
delete filters[call.name];
return;
}
if (typeof(callback) !== 'function') {
throw new Error('invalid callback');
}
// Already have a filter, just update the callback
if (info) {
info.callback = callback;
return;
}
info = {callback: callback};
filters[call.name] = info;
// Start a new filter
/*
info.filter = web3.eth.filter({
address: contractAddress,
topics: call.topics
}, function(error, result) {
// @TODO: Emit errors to .onerror? Maybe?
if (error) {
console.log(error);
return;
}
try {
info.callback.apply(self, call.parse(result.data));
} catch(error) {
console.log(error);
}
});
*/
}
function runMethod(method, estimateOnly) {
return function() {
var provider = wallet._provider;
var transaction = {}
var params = Array.prototype.slice.call(arguments);
if (params.length == contractInterface[method].inputs.length + 1) {
transaction = params.pop();
if (typeof(transaction) !== 'object') {
throw new Error('invalid transaction overrides');
}
for (var key in transaction) {
if (!allowedTransactionKeys[key]) {
throw new Error('unknown transaction override ' + key);
}
}
}
var call = contractInterface[method].apply(contractInterface, params);
switch (call.type) {
case 'call':
['data', 'gasLimit', 'gasPrice', 'to', 'value'].forEach(function(key) {
if (transaction[key] != null) {
throw new Error('call cannot override ' + key) ;
}
});
transaction.data = call.data;
if (transaction.from == null) {
transaction.from = wallet.address;
}
transaction.to = contractAddress;
if (estimateOnly) {
return new Promise(function(resolve, reject) {
resolve(new utils.BN(0));
});
}
return new Promise(function(resolve, reject) {
provider.call(transaction).then(function(value) {
resolve(call.parse(value));
}, function(error) {
reject(error);
});
});
case 'transaction':
['data', 'from', 'to'].forEach(function(key) {
if (transaction[key] != null) {
throw new Error('transaction cannot override ' + key) ;
}
});
transaction.data = call.data;
transaction.to = contractAddress;
if (transaction.gasLimit == null) {
transaction.gasLimit = 3000000;
}
if (estimateOnly) {
return new Promise(function(resolve, reject) {
provider.estimateGas(transaction).then(function(gasEstimate) {
resolve(gasEstimate);
}, function(error) {
reject(error);
});
});
}
return new Promise(function(resolve, reject) {
Promise.all([
provider.getTransactionCount(wallet.address, 'pending'),
provider.getGasPrice(),
]).then(function(results) {
if (transaction.nonce == null) {
transaction.nonce = results[0];
} else if (console.warn) {
console.warn('Overriding suggested nonce: ' + results[0]);
}
if (transaction.gasPrice == null) {
transaction.gasPrice = results[1];
} else if (console.warn) {
console.warn('Overriding suggested gasPrice: ' + utils.hexlify(results[1]));
}
var signedTransaction = wallet.sign(transaction);
provider.sendTransaction(signedTransaction).then(function(txid) {
resolve(txid);
}, function(error) {
reject(error);
});
}, function(error) {
reject(error);
});
});
}
};
}
var estimate = {};
utils.defineProperty(this, 'estimate', estimate);
contractInterface.methods.forEach(function(method) {
utils.defineProperty(this, method, runMethod(method, false));
utils.defineProperty(estimate, method, runMethod(method, true));
}, this);
contractInterface.events.forEach(function(method) {
var call = contractInterface[method].apply(contractInterface, []);
Object.defineProperty(self, 'on' + call.name.toLowerCase(), {
enumerable: true,
get: function() {
//console.log('get');
var info = filters[call.name];
if (!info || !info[call.name]) { return null; }
return info.callback;
},
set: function(value) {
//console.log('set');
setupFilter(call, value);
}
});
}, this);
}
utils.defineProperty(Contract, 'Interface', Interface);
module.exports = Contract;

331
lib/providers.js Normal file
View File

@@ -0,0 +1,331 @@
'use strict';
var inherits = require('inherits');
var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
var utils = require('./utils.js');
// The required methods a provider must support
var methods = [
'getBalance',
'getTransactionCount',
'getGasPrice',
'sendTransaction',
'call',
'estimateGas'
];
// Manages JSON-RPC to an Ethereum node
function Web3Connector(provider) {
if (!(this instanceof Web3Connector)) { throw new Error('missing new'); }
var nextMessageId = 1;
utils.defineProperty(this, 'sendMessage', function(method, params) {
return new Promise(function(resolve, reject) {
provider.sendAsync({
id: (nextMessageId++),
jsonrpc: '2.0',
method: method,
params: params
}, function(error, result) {
if (error) {
reject(error);
} else {
if (result.error) {
var error = new Error(result.error.message);
error.code = result.error.code;
error.data = result.error.data;
reject(error);
} else {
resolve(result.result);
}
}
});
});
});
}
// Mimics Web3 interface
function rpcSendAsync(url) {
return {
sendAsync: function(payload, callback) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type','application/json');
request.onreadystatechange = function() {
if (request.readyState !== 4) { return; }
if (typeof(callback) !== 'function') { return; }
var result = request.responseText;
try {
callback(null, JSON.parse(result));
} catch (error) {
var responseError = new Error('invalid response');
responseError.orginialError = error;
responseError.data = result;
callback(responseError);
}
};
try {
request.send(JSON.stringify(payload));
} catch (error) {
var connectionError = new Error('connection error');
connectionError.error = error;
callback(connectionError);
}
}
}
}
function SendAsyncProvider(sendAsync) {
if (!(this instanceof SendAsyncProvider)) { throw new Error('missing new'); }
utils.defineProperty(this, 'client', new Web3Connector(sendAsync));
}
function validBlock(value) {
if (value == null) { return 'latest'; }
if (value === 'latest' || value === 'pending') { return value; }
if (typeof(value) === 'number' && value == parseInt(value)) {
return parseInt(value);
}
throw new Error('invalid blockNumber');
}
function postProcess(client, method, params, makeBN) {
return new Promise(function(resolve, reject) {
client.sendMessage(method, params).then(function (result) {
if (!utils.isHexString(result)) {
reject(new Error('invalid server response'));
} else {
result = result.substring(2);
if (makeBN) {
result = new utils.BN(result, 16);
} else {
result = parseInt(result, 16);
}
resolve(result);
}
}, function(error) {
reject(error);
});
});
}
utils.defineProperty(SendAsyncProvider.prototype, 'getBalance', function(address, blockNumber) {
return postProcess(this.client, 'eth_getBalance', [
utils.getAddress(address),
validBlock(blockNumber)
], true);
});
utils.defineProperty(SendAsyncProvider.prototype, 'getTransactionCount', function(address, blockNumber) {
return postProcess(this.client, 'eth_getTransactionCount', [
utils.getAddress(address),
validBlock(blockNumber)
], false);
});
utils.defineProperty(SendAsyncProvider.prototype, 'getGasPrice', function() {
return postProcess(this.client, 'eth_gasPrice', [], true);
});
utils.defineProperty(SendAsyncProvider.prototype, 'sendTransaction', function(signedTransaction) {
if (!utils.isHexString(signedTransaction)) { throw new Error('invalid transaction'); }
return this.client.sendMessage('eth_sendRawTransaction', [signedTransaction]);
});
utils.defineProperty(SendAsyncProvider.prototype, 'call', function(transaction) {
// @TODO: check validTransaction?
return this.client.sendMessage('eth_call', [transaction]);
});
utils.defineProperty(SendAsyncProvider.prototype, 'estimateGas', function(transaction) {
// @TODO: check validTransaction?
return postProcess(this.client, 'eth_estimateGas', [transaction], true);
});
var providers = {};
function HttpProvider(url) {
if (!(this instanceof HttpProvider)) { throw new Error('missing new'); }
SendAsyncProvider.call(this, rpcSendAsync(url));
}
inherits(HttpProvider, SendAsyncProvider);
utils.defineProperty(providers, 'HttpProvider', HttpProvider);
function Web3Provider(provider) {
if (!(this instanceof Web3Provider)) { throw new Error('missing new'); }
if (provider.currentProvider) { provider = provider.currentProvider; }
if (!provider.sendAsync) { throw new Error('invalid provider'); }
SendAsyncProvider.call(this, provider);
}
inherits(Web3Provider, SendAsyncProvider);
utils.defineProperty(providers, 'Web3Provider', Web3Provider);
function base10ToBN(value) {
return new utils.BN(value);
}
function hexToBN(value) {
return new utils.BN(ensureHex(value).substring(2), 16);
}
function hexToNumber(value) {
if (!utils.isHexString(value)) { throw new Error('invalid hex string'); }
return parseInt(value.substring(2), 16);
}
function ensureHex(value) {
if (!utils.isHexString(value)) { throw new Error('invalid hex string'); }
return value;
}
function ensureTxid(value) {
if (!utils.isHexString(value, 32)) { throw new Error('invalid hex string'); }
return value;
}
function getGasPrice(value) {
if (!value || !value.transactions || value.transactions.length === 0) {
throw new Error('invalid response');
}
return hexToBN(value.transactions[0].gasPrice);
}
function EtherscanProvider(options) {
if (!(this instanceof EtherscanProvider)) { throw new Error('missing new'); }
if (!options) { options = {}; }
var testnet = options.testnet;
var apiKey = options.apiKey;
utils.defineProperty(this, 'testnet', testnet);
utils.defineProperty(this, 'apiKey', apiKey);
utils.defineProperty(this, '_send', function(query, check) {
var url = (testnet ? 'https://testnet.etherscan.io/api?': 'https://api.etherscan.io/api?');
url += query;
if (apiKey) { url += 'apikey=' + apiKey; }
//console.log('URL', url);
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onreadystatechange = function() {
if (request.readyState !== 4) { return; }
var result = request.responseText;
//console.log(result);
try {
result = JSON.parse(result);
if (result.message) {
if (result.message === 'OK') {
resolve(check(result.result));
} else {
reject(new Error('invalid response'));
}
} else {
if (result.error) {
console.log(result.error);
reject(new Error('invalid response'));
} else {
resolve(check(result.result));
}
}
} catch (error) {
console.log(error);
reject(new Error('invalid response'));
}
}
try {
request.send();
} catch (error) {
var connectionError = new Error('connection error');
connectionError.error = error;
reject(connectionError);
}
});
});
}
utils.defineProperty(providers, 'EtherscanProvider', EtherscanProvider);
utils.defineProperty(EtherscanProvider.prototype, 'getBalance', function(address, blockNumber) {
address = utils.getAddress(address);
blockNumber = validBlock(blockNumber);
var query = ('module=account&action=balance&address=' + address + '&tag=' + blockNumber);
return this._send(query, base10ToBN);
});
utils.defineProperty(EtherscanProvider.prototype, 'getTransactionCount', function(address, blockNumber) {
address = utils.getAddress(address);
blockNumber = validBlock(blockNumber);
var query = ('module=proxy&action=eth_getTransactionCount&address=' + address + '&tag=' + blockNumber);
return this._send(query, hexToNumber);
});
utils.defineProperty(EtherscanProvider.prototype, 'getGasPrice', function() {
var query = ('module=proxy&action=eth_gasPrice');
return this._send(query, hexToBN);
});
utils.defineProperty(EtherscanProvider.prototype, 'sendTransaction', function(signedTransaction) {
if (!utils.isHexString(signedTransaction)) { throw new Error('invalid transaction'); }
var query = ('module=proxy&action=eth_sendRawTransaction&hex=' + signedTransaction);
return this._send(query, ensureTxid);
});
utils.defineProperty(EtherscanProvider.prototype, 'call', function(transaction) {
var address = utils.getAddress(transaction.to);
var data = transaction.data;
if (!utils.isHexString(data)) { throw new Error('invalid data'); }
var query = ('module=proxy&action=eth_call&to=' + address + '&data=' + data);
return this._send(query, ensureHex);
});
utils.defineProperty(EtherscanProvider.prototype, 'estimateGas', function(transaction) {
var address = utils.getAddress(transaction.to);
var query = 'module=proxy&action=eth_estimateGas&to=' + address;
if (transaction.gasPrice) {
query += '&gasPrice=' + utils.hexlify(transaction.gasPrice);
}
if (transaction.gasLimit) {
query += '&gas=' + utils.hexlify(transaction.gasLimit);
}
if (transaction.from) {
query += '&from=' + utils.getAddress(transaction.from);
}
if (transaction.data) {
query += '&data=' + ensureHex(transaction.data);
}
if (transaction.value) {
query += '&value=' + utils.hexlify(transaction.value);
}
return this._send(query, hexToBN);
});
utils.defineProperty(providers, 'isProvider', function(provider) {
if (!provider) { return false; }
for (var i = 0; i < methods; i++) {
if (typeof(provider[methods[i]]) !== 'function') {
return false;
}
}
return true;
});
module.exports = providers;

4
lib/random-bytes.js Normal file
View File

@@ -0,0 +1,4 @@
'use strict';
module.exports = require('crypto').randomBytes;

74
lib/randomish.js Normal file
View File

@@ -0,0 +1,74 @@
'use strict';
var aes = require('aes-js');
var randomBytes = require('./random-bytes.js');
var utils = require('./utils.js');
function Randomish() {
if (!(this instanceof Randomish)) { throw new Error('missing new'); }
var weak = (randomBytes._weakCrypto || false);
var entropyBits = (weak ? 0: ((32 + 16) * 8));
Object.defineProperty(this, 'entropy', {
enumerable: true,
get: function() { return entropyBits; }
});
var entropy = new aes.ModeOfOperation.cbc(
Randomish.randomishBytes(32),
Randomish.randomishBytes(16)
);
utils.defineProperty(this, 'feedEntropy', function(data, expectedEntropyBits) {
if (!data) { data = ''; }
if (!expectedEntropyBits) { expectedEntropyBits = 0; }
if (parseInt(expectedEntropyBits) != expectedEntropyBits) {
throw new Error('invalid expectedEntropyBits');
}
data = (new Date()).getTime() + '-' + JSON.stringify(data) + '-' + data.toString();
var hashed = utils.sha3(new Buffer(data, 'utf8'));
entropyBits += expectedEntropyBits + (weak ? 0: ((32) * 8));
// Feed the hashed data and random data to the mode of operation
entropy.encrypt(hashed.slice(0, 16));
entropy.encrypt(randomBytes(16));
entropy.encrypt(hashed.slice(0, 16));
return new Buffer(entropy.encrypt(randomBytes(16)));
});
utils.defineProperty(this, 'randomBytes', function(length, key) {
if (parseInt(length) != length || length <= 0 || length > 1024) {
throw new Error('invalid length');
}
// If we don't have a key, create one
if (!key) {
key = Buffer.concat([this.feedEntropy(), this.feedEntropy()]);
}
if (!Buffer.isBuffer(key) || key.length !== 32) {
throw new Error('invalid key');
}
var aesCbc = new aes.ModeOfOperation.cbc(key, this.feedEntropy());
var result = new Buffer(0);
while (result.length < length) {
result = Buffer.concat([result, this.feedEntropy()]);
}
return result.slice(0, length);
});
this.feedEntropy();
}
utils.defineProperty(Randomish, 'randomishBytes', function(length) {
return randomBytes(length);
});
module.exports = Randomish

345
lib/secret-storage.js Normal file
View File

@@ -0,0 +1,345 @@
'use strict';
var aes = require('aes-js');
var pbkdf2 = require('pbkdf2');
var scrypt = require('scrypt-js');
var uuid = require('uuid');
var Randomish = require('./randomish.js');
var SigningKey = require('./signing-key.js');
var utils = require('./utils.js')
// 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;
}
/*
function SecretStorage(json, signingKey) {
if (!(this instanceof SecretStorage)) { throw new Error('missing new'); }
utils.defineProperty(this, 'json', json);
Object.defineProperty(this, 'data', {
enumerable: true,
get: function() { return JSON.parse(json); }
});
utils.defineProperty(this, 'address', signingKey.privateKey);
utils.defineProperty(this, 'signingKey', signingKey);
}
*/
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);
// Ethereum Address
var ethaddr = utils.getAddress(searchPath(data, 'ethaddr'));
// Encrypted Seed
var encseed = new Buffer(searchPath(data, 'encseed'), 'hex');
if (!encseed || (encseed.length % 16) !== 0) {
throw new Error('invalid encseed');
}
var key = pbkdf2.pbkdf2Sync(password, password, 2000, 32, 'sha256').slice(0, 16);
var iv = encseed.slice(0, 16);
var encryptedSeed = encseed.slice(16);
// Decrypt the seed
var seed = new Buffer(0);
var aesCbc = new aes.ModeOfOperation.cbc(key, iv);
for (var i = 0; i < encryptedSeed.length; i += 16) {
seed = Buffer.concat([seed, new Buffer(aesCbc.decrypt(encryptedSeed.slice(i, i + 16)))]);
}
// Check PKCS#7 padding is valid
var pad = seed[seed.length - 1];
if (pad > 16 || pad > seed.length) {
throw new Error('invalid password');
}
for (var i = seed.length - pad; i < seed.length; i++) {
if (seed[i] !== pad) {
throw new Error('invalid password');
}
}
// Strip the padding
seed = seed.slice(0, seed.length - pad);
// 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 signingKey = new SigningKey(utils.sha3(new Buffer(seedHex)));
if (signingKey.address !== ethaddr) {
throw new Error('corrupt crowdsale wallet');
}
return signingKey;
});
utils.defineProperty(secretStorage, 'decrypt', function(json, password, progressCallback) {
if (!Buffer.isBuffer(password)) { throw new Error('password must be a buffer'); }
var data = JSON.parse(json);
var decrypt = function(key, ciphertext) {
var cipher = searchPath(data, 'crypto/cipher');
if (cipher === 'aes-128-ctr') {
var iv = new Buffer(searchPath(data, 'crypto/cipherparams/iv'), 'hex')
var counter = new aes.Counter(iv);
var aesCtr = new aes.ModeOfOperation.ctr(key, counter);
return new Buffer(aesCtr.decrypt(ciphertext));
}
return null;
};
var computeMAC = function(derivedHalf, ciphertext) {
return utils.sha3(Buffer.concat([derivedHalf, ciphertext]));
}
return new Promise(function(resolve, reject) {
var kdf = searchPath(data, 'crypto/kdf');
if (kdf && kdf.toLowerCase() === 'scrypt') {
var salt = new Buffer(searchPath(data, 'crypto/kdfparams/salt'), 'hex');
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 = searchPath(data, 'crypto/kdfparams/dklen');
if (dkLen !== 32) {
reject( new Error('unsupported key-derivation derived-key length'));
return;
}
scrypt(password, salt, N, r, p, dkLen, function(error, progress, key) {
if (error) {
error.progress = progress;
reject(error);
} else if (key) {
key = new Buffer(key);
var ciphertext = new Buffer(searchPath(data, 'crypto/ciphertext'), 'hex');
var computedMAC = computeMAC(key.slice(16, 32), ciphertext).toString('hex').toLowerCase();
if (computedMAC !== searchPath(data, 'crypto/mac').toLowerCase()) {
reject(new Error('invalid password'));
return;
}
var privateKey = decrypt(key.slice(0, 16), ciphertext);
if (!privateKey) {
reject(new Error('unsupported cipher'));
return;
}
var signingKey = new SigningKey(privateKey);
if (signingKey.address !== utils.getAddress(data.address)) {
reject(new Error('address mismatch'));
return;
}
if (progressCallback) { progressCallback(1); }
resolve(signingKey);
} else if (progressCallback) {
return progressCallback(progress);
}
});
} else {
// @TOOD: Support pbkdf2 kdf
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 = utils.hexOrBuffer(privateKey, 'private key');
if (privateKey.length !== 32) { throw new Erro('invalid private key'); }
// Check the password
if (!Buffer.isBuffer(password)) { throw new Error('password must be a buffer'); }
// Check/generate the salt
var salt = options.salt;
if (salt) {
salt = utils.hexOrBuffer(salt, 'salt');
} else {
salt = (new Randomish()).randomBytes(32);;
}
// Override initialization vector
var iv = null;
if (options.iv) {
iv = utils.hexOrBuffer(options.iv, 'iv');
if (iv.length !== 16) { throw new Error('invalid iv'); }
}
// Override the uuid
var uuidRandom = options.uuid;
if (uuidRandom) {
uuidRandom = utils.hexOrBuffer(uuidRandom, 'uuid');
if (uuidRandom.length !== 16) { throw new Error('invalid uuid'); }
}
// 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)
// - 16 bytes The initialization vector
// - 16 bytes The UUID random bytes
scrypt(password, salt, N, r, p, 64, function(error, progress, key) {
if (error) {
error.progress = progress;
reject(error);
} else if (key) {
// Convert the array-like to a Buffer
key = new Buffer(key);
// These will be used to encrypt the wallet (as per Web3 secret storage)
var derivedKey = key.slice(0, 16);
var macPrefix = key.slice(16, 32);
// Get the initialization vector
if (!iv) { iv = key.slice(32, 48); }
// Get the UUID random data
if (!uuidRandom) { uuidRandom = key.slice(48, 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 = new Buffer(aesCtr.encrypt(privateKey));
// Compute the message authentication code, used to check the password
var mac = utils.sha3(Buffer.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: iv.toString('hex')
},
ciphertext: ciphertext.toString('hex'),
kdf: 'scrypt',
kdfparams: {
salt: salt.toString('hex'),
n: N,
dklen: 32,
p: p,
r: r
},
mac: mac.toString('hex')
}
};
if (progressCallback) { progressCallback(1); }
resolve(JSON.stringify(data));
} else if (progressCallback) {
return progressCallback(progress);
}
});
});
});
module.exports = secretStorage;

38
lib/signing-key.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict';
var elliptic = require('elliptic');
var utils = require('./utils.js');
var secp256k1 = new (elliptic.ec)('secp256k1');
function SigningKey(privateKey) {
if (!(this instanceof SigningKey)) { throw new Error('missing new'); }
if (utils.isHexString(privateKey, 32)) {
privateKey = utils.hexOrBuffer(privateKey);
} else if (!Buffer.isBuffer(privateKey) || privateKey.length !== 32) {
throw new Error('invalid private key');
}
utils.defineProperty(this, 'privateKey', '0x' + privateKey.toString('hex'))
var keyPair = secp256k1.keyFromPrivate(privateKey);
var publicKey = (new Buffer(keyPair.getPublic(false, 'hex'), 'hex')).slice(1);
var address = utils.getAddress(utils.sha3(publicKey).slice(12).toString('hex'));
utils.defineProperty(this, 'address', address)
utils.defineProperty(this, 'signDigest', function(digest) {
return keyPair.sign(digest, {canonical: true});
});
}
utils.defineProperty(SigningKey, 'recover', function(digest, r, s, recoveryParam) {
var publicKey = secp256k1.recoverPubKey(digest, {r: r, s: s}, recoveryParam);
publicKey = (new Buffer(publicKey.encode('hex', false), 'hex')).slice(1);
return utils.getAddress(utils.sha3(publicKey).slice(12).toString('hex'));
});
module.exports = SigningKey;

78
lib/units.js Normal file
View File

@@ -0,0 +1,78 @@
var utils = require('./utils.js');
var zero = new utils.BN(0);
var negative1 = new utils.BN(-1);
var tenPower18 = new utils.BN('1000000000000000000');
function formatEther(wei, options) {
if (typeof(wei) === 'number') {
// @TODO: Warn if truncation will occur?
wei = new utils.BN(wei);
} else if (utils.isHexString(wei)) {
wei = new utils.BN(wei.substring(2), 16);
}
if (!options) { options = {}; }
if (!(wei instanceof utils.BN)) { throw new Error('invalid wei'); }
var negative = wei.lt(zero);
if (negative) { wei = wei.mul(negative1); }
var fraction = wei.mod(tenPower18).toString(10);
while (fraction.length < 18) { fraction = '0' + fraction; }
if (!options.pad) {
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
}
var whole = wei.div(tenPower18).toString(10);
if (options.commify) {
whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}
var value = whole + '.' + fraction;
if (negative) { value = '-' + value; }
return value;
}
function parseEther(ether) {
if (typeof(ether) !== 'string' || !ether.match(/^-?[0-9.]+$/)) {
throw new Error('invalid value');
}
// Is it negative?
var negative = (ether.substring(0, 1) === '-');
if (negative) { ether = ether.substring(1); }
if (ether === '.') { throw new Error('invalid value'); }
// Split it into a whole and fractional part
var comps = ether.split('.');
if (comps.length > 2) { throw new Error('too many decimal points'); }
var whole = comps[0], fraction = comps[1];
if (!whole) { whole = '0'; }
if (!fraction) { fraction = '0'; }
if (fraction.length > 18) { throw new Error('too many decimal places'); }
while (fraction.length < 18) { fraction += '0'; }
whole = new utils.BN(whole);
fraction = new utils.BN(fraction);
var wei = (whole.mul(tenPower18)).add(fraction);
if (negative) { wei = wei.mul(negative1); }
return wei;
}
module.exports = {
formatEther: formatEther,
parseEther: parseEther,
}

View File

@@ -1,5 +1,9 @@
'use strict';
var rlp = require('rlp');
var BN = require('../node_modules/elliptic/node_modules/bn.js/lib/bn.js');
var hash = require('../node_modules/elliptic/node_modules/hash.js/lib/hash.js');
// See: https://github.com/emn178/js-sha3
@@ -180,8 +184,18 @@ function keccak(message) {
};
// Quick sanity check
if (keccak(new Buffer('ricmoo')) !== 'b05e424817fb90aa7a79e9da5c5f94070a316219c6ebb863a9ff7ca357dc9fa9') {
throw new Error('problem with sh3?!');
//if (keccak(new Buffer('ricmoo')) !== 'b05e424817fb90aa7a79e9da5c5f94070a316219c6ebb863a9ff7ca357dc9fa9') {
// throw new Error('problem with sh3?!');
//}
function sha256(data) {
if (typeof(data) === 'string') {
data = new Buffer(data, 'utf8');
} else if (!Buffer.isBuffer(data)) {
throw new Error('must be a sting');
}
return (new Buffer(hash.sha256().update(data).digest('hex'), 'hex'));
}
function sha3(data) {
@@ -201,6 +215,119 @@ function defineProperty(object, name, value) {
});
}
function getChecksumAddress(address) {
if (typeof(address) !== 'string' || !address.match(/^0x[0-9A-Fa-f]{40}$/)) {
throw new Error('invalid address');
}
address = address.substring(2).toLowerCase();
var hashed = sha3(address);
address = address.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('');
}
function getAddress(address) {
var result = null;
if (typeof(address) !== 'string') { throw new Error('invalid 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) {
throw new Error('invalid address checksum');
}
// 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)) {
throw new Error('invalid address icap checksum');
}
result = (new BN(address.substring(4), 36)).toString(16);
while (result.length < 40) { result = '0' + result; }
result = getChecksumAddress('0x' + result);
} else {
throw new Error('invalid address');
}
return result;
}
// 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(Math.log10(Number.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 getIcapAddress(address) {
address = getAddress(address).substring(2);
var base36 = (new BN(address, 16)).toString(36).toUpperCase();
while (base36.length < 30) { base36 = '0' + base36; }
return 'XE' + ibanChecksum('XE00' + base36) + base36;
}
// http://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed
function getContractAddress(transaction) {
return getAddress('0x' + sha3(rlp.encode([
hexOrBuffer(getAddress(transaction.from)),
hexOrBuffer(hexlify(transaction.nonce, 'nonce'))
])).slice(12).toString('hex'));
}
function cloneObject(object) {
var clone = {};
for (var key in object) { clone[key] = object[key]; }
return clone;
}
function stripZeros(buffer) {
var i = 0;
for (i = 0; i < buffer.length; i++) {
@@ -215,9 +342,17 @@ function bnToBuffer(bn) {
return stripZeros(new Buffer(hex, 'hex'));
}
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;
}
function hexOrBuffer(value, name) {
if (!Buffer.isBuffer(value)) {
if (typeof(value) !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) {
if (!isHexString(value)) {
var error = new Error(name ? ('invalid ' + name) : 'invalid hex or buffer');
error.reason = 'invalid hex string';
error.value = value;
@@ -249,10 +384,19 @@ module.exports = {
defineProperty: defineProperty,
getAddress: getAddress,
getIcapAddress: getIcapAddress,
getContractAddress: getContractAddress,
cloneObject: cloneObject,
bnToBuffer: bnToBuffer,
isHexString: isHexString,
hexOrBuffer: hexOrBuffer,
hexlify: hexlify,
stripZeros: stripZeros,
sha256: sha256,
sha3: sha3,
}

283
lib/wallet.js Normal file
View File

@@ -0,0 +1,283 @@
'use strict';
var rlp = require('rlp');
var Contract = require('./contract.js');
var providers = require('./providers.js');
var SigningKey = require('./signing-key.js');
var utils = require('./utils.js');
// This ensures we inject a setImmediate into the global space, which
// dramatically improves the performance of the scrypt PBKDF.
require('setimmediate');
var transactionFields = [
{name: 'nonce', maxLength: 32, },
{name: 'gasPrice', maxLength: 32, },
{name: 'gasLimit', maxLength: 32, },
{name: 'to', length: 20, },
{name: 'value', maxLength: 32, },
{name: 'data'},
];
function Wallet(privateKey, provider) {
if (!(this instanceof Wallet)) { throw new Error('missing new'); }
// Make sure we have a valid signing key
var signingKey = privateKey;
if (!(privateKey instanceof SigningKey)) {
signingKey = new SigningKey(privateKey);
}
utils.defineProperty(this, 'privateKey', signingKey.privateKey);
Object.defineProperty(this, 'provider', {
enumerable: true,
get: function() { return provider; },
set: function(value) {
if (value !== null && !providers.isProvider(value)) {
throw new Error('invalid provider');
}
provider = value;
}
});
Object.defineProperty(this, '_provider', {
enumerable: true,
get: function() {
if (!provider) { throw new Error('missing provider'); }
return provider;
},
});
if (provider !== null) {
// If no provider was provided, check for metamask or ilk
if (provider === undefined) {
if (global.web3 && global.web3.currentProvider && global.web3.currentProvider.sendAsync) {
this.provider = new providers.Web3Provider(global.web3.currentProvider);
}
// An Ethereum RPC node
} else if (typeof(provider) === 'string' && provider.match(/^https?:\/\//)) {
this.provider = new providers.HttpProvider(provider);
} else {
this.provider = provider;
}
}
utils.defineProperty(this, 'address', signingKey.address);
utils.defineProperty(this, 'sign', function(transaction) {
var raw = [];
transactionFields.forEach(function(fieldInfo) {
var value = transaction[fieldInfo.name] || (new Buffer(0));
value = utils.hexOrBuffer(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(value);
});
var digest = utils.sha3(rlp.encode(raw));
var signature = signingKey.signDigest(digest);
var s = signature.s;
var v = signature.recoveryParam;
raw.push(new Buffer([27 + v]));
raw.push(utils.bnToBuffer(signature.r));
raw.push(utils.bnToBuffer(s));
return ('0x' + rlp.encode(raw).toString('hex'));
});
}
utils.defineProperty(Wallet, 'parseTransaction', function(rawTransaction) {
rawTransaction = utils.hexOrBuffer(rawTransaction, 'rawTransaction');
var signedTransaction = rlp.decode(rawTransaction);
var raw = [];
var transaction = {};
transactionFields.forEach(function(fieldInfo, index) {
transaction[fieldInfo.name] = signedTransaction[index];
raw.push(signedTransaction[index]);
});
if (transaction.to) {
if (transaction.to.length === 0) {
delete transaction.to;
} else {
transaction.to = utils.getAddress('0x' + transaction.to.toString('hex'));
}
}
['gasPrice', 'gasLimit', 'nonce', 'value'].forEach(function(name) {
if (!transaction[name]) { return; }
if (transaction[name].length === 0) {
transaction[name] = new utils.BN(0);
} else {
transaction[name] = new utils.BN(transaction[name].toString('hex'), 16);
}
});
/* @TODO: Maybe? In the future, all nonces stored as numbers? (obviously, major version change)
if (transaction.nonce) {
transaction.nonce = transaction.nonce.toNumber()
}
*/
if (signedTransaction.length > 6 && signedTransaction[6].length === 1 &&
signedTransaction[7].length >= 1 && signedTransaction[7].length <= 32 &&
signedTransaction[8].length >= 1 && signedTransaction[7].length <= 32) {
transaction.v = signedTransaction[6][0];
transaction.r = signedTransaction[7];
transaction.s = signedTransaction[8];
var digest = utils.sha3(rlp.encode(raw));
try {
transaction.from = SigningKey.recover(digest, transaction.r, transaction.s, transaction.v - 27);
} catch (error) { }
}
return transaction;
});
utils.defineProperty(Wallet.prototype, 'getBalance', function(blockNumber) {
var provider = this._provider;
var self = this;
return new Promise(function(resolve, reject) {
provider.getBalance(self.address, blockNumber).then(function(balance) {
resolve(balance);
}, function(error) {
reject(error);
});
});
});
utils.defineProperty(Wallet.prototype, 'getTransactionCount', function(blockNumber) {
var provider = this._provider;
var self = this;
return new Promise(function(resolve, reject) {
provider.getTransactionCount(self.address, blockNumber).then(function(transactionCount) {
resolve(transactionCount);
}, function(error) {
reject(error);
});
});
});
utils.defineProperty(Wallet.prototype, 'estimateGas', function(transaction) {
var provider = this._provider;
transaction = utils.cloneObject(transaction);
if (transaction.from == null) { transaction.from = this.address; }
return new Promise(function(resolve, reject) {
provider.estimateGas(transaction).then(function(gasEstimate) {
resolve(gasEstimate);
}, function(error) {
reject(error);
});
});
});
utils.defineProperty(Wallet.prototype, 'sendTransaction', function(transaction) {
var gasLimit = transaction.gasLimit;
if (gasLimit == null) { gasLimit = 3000000; }
var self = this;
var provider = this._provider;
var gasPrice = new Promise(function(resolve, reject) {
if (transaction.gasPrice) {
return resolve(transaction.gasPrice);
}
provider.getGasPrice().then(function(gasPrice) {
resolve(gasPrice);
}, function(error) {
reject(error);
});
});
var nonce = new Promise(function(resolve, reject) {
if (transaction.nonce) {
return resolve(transaction.nonce);
}
provider.getTransactionCount(self.address, 'pending').then(function(transactionCount) {
resolve(transactionCount);
}, function(error) {
reject(error);
});
});
return new Promise(function(resolve, reject) {
Promise.all([gasPrice, nonce]).then(function(results) {
var signedTransaction = self.sign({
to: transaction.to,
gasLimit: gasLimit,
gasPrice: results[0],
nonce: results[1],
value: transaction.value
});
provider.sendTransaction(signedTransaction).then(function(txid) {
resolve(txid);
}, function(error) {
reject(error);
});
}, function(error) {
reject(error);
});
});
});
utils.defineProperty(Wallet.prototype, 'send', function(address, amountWei, options) {
address = utils.getAddress(address);
if (utils.BN.isBN(amountWei)) {
amountWei = '0x' + utils.bnToBuffer(amountWei).toString('hex');
}
if (!utils.isHexString(amountWei)) { throw new Error('invalid amountWei'); }
if (!options) { options = {}; }
return this.sendTransaction({
to: address,
gasLimit: options.gasLimit,
gasPrice: options.gasPrice,
nonce: options.nonce,
value: amountWei,
});
});
utils.defineProperty(Wallet.prototype, 'getContract', function(address, abi) {
return new Contract(this, address, new Contract.Interface(abi));
});
utils.defineProperty(Wallet, '_Contract', Contract);
module.exports = Wallet;

View File

@@ -1,32 +1,48 @@
{
"name": "ethers-wallet",
"version": "0.0.1",
"version": "1.0.5",
"description": "Ethereum wallet library.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "./node_modules/.bin/nodeunit tests/index.js",
"version": "grunt dist"
},
"dependencies": {
"aes-js": "0.2.4",
"aes-js": "2.0.0",
"elliptic": "6.3.1",
"inherits": "2.0.1",
"pbkdf2": "3.0.4",
"rlp": "2.0.0",
"scrypt-js": "2.0.0",
"uuid": "2.0.1"
"setimmediate": "1.0.4",
"scrypt-js": "2.0.3",
"uuid": "2.0.1",
"xmlhttprequest": "1.8.0"
},
"browser": {
"./lib/random-bytes.js": "./lib/browser-random-bytes.js",
"xmlhttprequest": "./lib/browser-xmlhttprequest.js"
},
"devDependencies": {
"ethereumjs-abi": "0.6.2",
"ethereumjs-tx": "1.1.1",
"ethereumjs-util": "4.3.0",
"grunt": "^0.4.5",
"grunt-browserify": "^5.0.0",
"grunt-contrib-uglify": "^1.0.1",
"web3": "0.15.3"
"nodeunit": "0.9.1",
"web3": "0.15.3",
"ethereumjs-vm": "1.4.0",
"solc": "0.3.5"
},
"keywords": [
"ethereum",
"wallet"
],
"author": "Richard Moore <me@ricmoo.com>",
"repository": {
"type": "git",
"url": "git://github.com/ethers-io/ethers-wallet.git"
},
"license": "MIT"
}

View File

@@ -0,0 +1 @@
{"encseed": "e695362f72ab0a364385f35a3435300188300ee5220c03dcbcf461c62432b6679ab163abf476aefa5f8cf366eea73c8b7e28e8c0a79ef819634bb8ee1875253d05b3825076c514063a7ccd8f0f79ba352799571f8ec8b749f8508a638a9a891d", "ethaddr": "2e326fa404fc3661de4f4361776ed9bbabdc26e3", "email": "me@ricmoo.com", "btcaddr": "1GW9wXcbtFNV6N6Mxje82Tmjk2iB9fffaj"}

View File

@@ -0,0 +1 @@
{"encseed": "3a7cc89f7d53d44d6e6445a2cf7fa359598f7914e1b4f808a1b55a13ea4cc71a5529782ba169bb0b169b29e4160c2d4d3167ed282267634ae326379cdf400bf82bc9512be11169bc1822fef4a17b9cf4ad685544ddfc793eafee573650f19185", "ethaddr": "0b88d4b324ec24c8c078551e6e5075547157e5b6", "email": "me@ricmoo.com", "btcaddr": "13pey9DLvYxeftGaGF1xgTfPYNmhu1fbm3"}

View File

@@ -0,0 +1 @@
{"address":"88a5c2d9919e46f883eb62f7b8dd9d0cc45bc290","Crypto":{"cipher":"aes-128-ctr","ciphertext":"10adcc8bcaf49474c6710460e0dc974331f71ee4c7baa7314b4a23d25fd6c406","cipherparams":{"iv":"1dcdf13e49cea706994ed38804f6d171"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"bbfa53547e3e3bfcc9786a2cbef8504a5031d82734ecef02153e29daeed658fd"},"mac":"1cf53b5ae8d75f8c037b453e7c3c61b010225d916768a6b145adf5cf9cb3a703"},"id":"fb1280c0-d646-4e40-9550-7026b1be504a","version":3}

View File

@@ -0,0 +1 @@
{"address":"4a9cf99357f5789251a8d7fad5b86d0f31eeb938","Crypto":{"cipher":"aes-128-ctr","ciphertext":"f93a875c23f5430bb86962da1ff2176d5b9179cfdd31e93027d67b0123e7a059","cipherparams":{"iv":"5fb35020cc5e84447a4b8a3a39543dbe"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"0d6bf5361b8c70745b8d03bb096876b27f00de2627b503b3f7c5ce9e8e70abe6"},"mac":"8f80f79e55cc36c231a00a69c43d603bd0cf8eb70bb9d77532cd7ddb56e4707e"},"id":"372b47d8-aba7-41a5-b3cd-a848b4f5aab1","version":3}

View File

@@ -0,0 +1 @@
{"address":"17c5185167401ed00cf5f5b2fc97d9bbfdb7d025","id":"01234567-8901-4345-a789-012345678901","version":3,"Crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"deadbeef1deadbeef2deadbeef301234"},"ciphertext":"cea502df4b9405fa2b6d8d19ec4e2953c5ce08e0e01d4c0292512ce62baef8f3","kdf":"scrypt","kdfparams":{"salt":"abcd1abcd2abcd3abcd4abcd5abcd6ef","n":1024,"dklen":32,"p":2,"r":4},"mac":"6e0a8e2409261d464e35a251f4b1fddd6ad0f7045a18d3957c127387f1c0de72"}}

198
test.js
View File

@@ -1,198 +0,0 @@
// @TODO: Move to an actual unit test system
var crypto = require('crypto');
var ethereumTx = require('ethereumjs-tx');
var ethereumUtil = require('ethereumjs-util');
var iban = require('./node_modules/web3/lib/web3/iban.js');
var Wallet = require('./index.js');
function randomBytes(length) {
return crypto.randomBytes(length);
}
function randomHex(length) {
if (length === 0) { return undefined; }
var bytes = randomBytes(length);
var hex = bytes.toString('hex');
if (length) {
if (bytes[0] >= 128) {
hex = hex.toUpperCase();
} else {
hex = hex.toLowerCase();
}
}
return '0x' + hex;
}
(function() {
var abi = {
"SimpleStorage": [
{
"constant":true,
"inputs":[],
"name":"getValue",
"outputs":[{"name":"","type":"string"}],
"type":"function"
}, {
"constant":false,
"inputs":[{"name":"value","type":"string"}],
"name":"setValue",
"outputs":[],
"type":"function"
}, {
"anonymous":false,
"inputs":[
{"indexed":false,"name":"oldValue","type":"string"},
{"indexed":false,"name":"newValue","type":"string"}
],
"name":"valueChanged",
"type":"event"
}
]
}
/*
var privateKey = Wallet.utils.Buffer(32);
privateKey.fill(0x42);
var wallet = new Wallet(privateKey);
*/
var contract = new Wallet._Contract(abi.SimpleStorage);
var getValue = contract.getValue()
var setValue = contract.setValue("foobar");
var valueChanged = contract.valueChanged()
console.log(getValue, setValue, valueChanged);
})();
(function() {
for (var i = 0; i < 1000; i++) {
var privateKey = randomBytes(32);
var official = '0x' + ethereumUtil.privateToAddress(privateKey).toString('hex');
var ethers = (new Wallet(privateKey)).address;
if (ethers !== ethereumUtil.toChecksumAddress(official)) {
console.log(i);
console.log('A', official);
console.log('B', ethers);
throw new Error('What?');
}
}
})();
(function() {
function testAddress(address) {
var officialIban = (iban.fromAddress(address))._iban;
var ethersAddress = Wallet.getAddress(officialIban);
var officialAddress = ethereumUtil.toChecksumAddress(address)
if (officialAddress !== ethersAddress) {
console.log('A', officialAddress);
console.log('B', ethersAddress);
throw new Error('waht?! address');
}
var ethersIban = Wallet.getIcapAddress(address);
if (officialIban !== ethersIban) {
console.log('A', officialIban);
console.log('B', ethersIban);
throw new Error('waht?! icap');
}
}
testAddress('0x0000000000000000000000000000000000000000');
testAddress('0xffffffffffffffffffffffffffffffffffffffff');
for (var i = 0; i < 10000; i++) {
testAddress(randomHex(20));
}
})();
(function() {
function testAddress(address) {
var official = ethereumUtil.toChecksumAddress(address);
var ethers = Wallet.getAddress(address);
if (official !== ethers) {
console.log('A', official);
console.log('B', ethers);
throw new Error('waht?!');
}
}
testAddress('0x0000000000000000000000000000000000000000');
testAddress('0xffffffffffffffffffffffffffffffffffffffff');
for (var i = 0; i < 10000; i++) {
testAddress(randomHex(20));
}
})();
(function() {
function testTransaction(privateKey, transaction, signature) {
var rawTransaction = new ethereumTx(transaction);
rawTransaction.sign(privateKey);
var official = '0x' + rawTransaction.serialize().toString('hex');
var ethers = (new Wallet(privateKey)).sign(transaction);
if (ethers !== official) {
console.log('A', ethers);
console.log('B', official);
throw new Error('What?');
}
}
for (var i = 0; i < 1000; i++) {
var transaction = {
to: randomHex(20),
data: randomHex(parseInt(10 * Math.random())),
gasLimit: randomHex(parseInt(10 * Math.random())),
gasPrice: randomHex(parseInt(10 * Math.random())),
value: randomHex(parseInt(10 * Math.random())),
nonce: randomHex(parseInt(10 * Math.random())),
};
testTransaction(randomBytes(32), transaction);
}
// See: https://github.com/ethereumjs/ethereumjs-tx/blob/master/test/txs.json
testTransaction(new Buffer('164122e5d39e9814ca723a749253663bafb07f6af91704d9754c361eb315f0c1', 'hex'), {
nonce: "0x",
gasPrice: "0x09184e72a000",
gasLimit: "0x2710",
to: "0x0000000000000000000000000000000000000000",
value: "0x",
data: "0x7f7465737432000000000000000000000000000000000000000000000000000000600057",
}, {
v: "0x1c",
r: "0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab",
s: "0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13"
});
testTransaction(new Buffer('e0a462586887362a18a318b128dbc1e3a0cae6d4b0739f5d0419ec25114bc722', 'hex'), {
nonce: "0x06",
gasPrice: "0x09184e72a000",
gasLimit: "0x01f4",
to: "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c",
value: "0x016345785d8a0000",
data: "0x",
}, {
v: "0x1c",
r: "0x24a484bfa7380860e9fa0a9f5e4b64b985e860ca31abd36e66583f9030c2e29d",
s: "0x4d5ef07d9e73fa2fbfdad059591b4f13d0aa79e7634a2bb00174c9200cabb04d"
});
testTransaction(new Buffer('164122e5d39e9814ca723a749253663bafb07f6af91704d9754c361eb315f0c1', 'hex'), {
nonce: "0x06",
gasPrice: "0x09184e72a000",
gasLimit: "0x0974",
to: "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c",
value: "0x016345785d8a0000",
data: "0x00000000000000000000000000000000000000000000000000000000000000ad000000000000000000000000000000000000000000000000000000000000fafa0000000000000000000000000000000000000000000000000000000000000dfa0000000000000000000000000000000000000000000000000000000000000dfa00000000000000000000000000000000000000000000000000000000000000ad000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000df000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000df000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000d",
}, {
v: "0x1c",
r: "0x5e9361ca27e14f3af0e6b28466406ad8be026d3b0f2ae56e3c064043fb73ec77",
s: "0x29ae9893dac4f9afb1af743e25fbb6a63f7879a61437203cb48c997b0fcefc3a"
});
})();

44
tests/index.js Normal file
View File

@@ -0,0 +1,44 @@
'use strict';
console.log('Running test cases... (this can take a long time, please be patient)');
// Test converting private keys to addresses
module.exports.testPrivateKeyToAddress = require('./test-privatekey.js');
// Test checksum addresses
module.exports.testChecksumAddress = require('./test-checksum-address.js');
// Test ICAP addresses
module.exports.testIcapAddress = require('./test-icap-address.js');
// Test ether strng formatting and parsing
module.exports.testEtherFormat = require('./test-ether-format.js');
// Test transactions
module.exports.testTrasactions = require('./test-transactions.js');
// Test Solidity encoding/decoding parameters
module.exports.testSolidityCoder = require('./test-solidity-coder.js');
// Test the contract meta-class
module.exports.testContracts = require('./test-contracts.js');
// Test the secret storage JSON wallet encryption/decryption
module.exports.testSecretStorage = require('./test-secret-storage.js');
// Test brain wallet generation
module.exports.testBrainWallet = require('./test-brain-wallet.js');
// Test contract address helper
module.exports.testContractAddress = require('./test-contract-address.js');
// Test the providers API (we still need to add a lot ore test cases here)
module.exports.testProviders = require('./test-providers.js');

View File

@@ -0,0 +1,16 @@
'use strict';
var Wallet = require('../index.js');
module.exports = function(test) {
var username = new Wallet.utils.Buffer('ricmoo', 'utf8');
var password = new Wallet.utils.Buffer('password', 'utf8');
Wallet.summonBrainWallet(username, password).then(function(wallet) {
test.equal(wallet.address, '0xbed9d2E41BdD066f702C4bDB86eB3A3740101acC', 'wrong wallet generated');
test.done();
}, function(error) {
test.ok(false, 'Failed to generarte brain wallet');
test.done();
});
}
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,27 @@
'use strict';
var Wallet = require('../index.js');
var ethereumUtil = require('ethereumjs-util');
var utils = require('./utils.js');
module.exports = function(test) {
function testAddress(address) {
var official = ethereumUtil.toChecksumAddress(address);
var ethers = Wallet.getAddress(address);
test.equal(ethers, official, 'wrong address');
}
test.expect(2 + 10000);
testAddress('0x0000000000000000000000000000000000000000');
testAddress('0xffffffffffffffffffffffffffffffffffffffff');
for (var i = 0; i < 10000; i++) {
testAddress(utils.randomHexString(20));
}
test.done();
};
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,21 @@
'use strict';
var Wallet = require('../index.js');
module.exports = function(test) {
// Transaction: 0x939aa17985bc2a52a0c1cba9497ef09e092355a805a8150e30e24b753bac6864
var transaction = {
from: '0xb2682160c482eb985ec9f3e364eec0a904c44c23',
nonce: 10,
}
test.equal(
Wallet.utils.getContractAddress(transaction),
Wallet.getAddress('0x3474627d4f63a678266bc17171d87f8570936622'),
'Failed to match contract address'
)
test.done();
}
module.exports.testSelf = module.exports;

87
tests/test-contracts.js Normal file
View File

@@ -0,0 +1,87 @@
'use strict';
var Wallet = require('../index.js');
module.exports = function(test) {
var contractAddress = '0xdfaf84077cF4bCECA4F79d167F47041Ed3006D5b';
var contractABI = {
"SimpleStorage": [
{
"constant":true,
"inputs":[],
"name":"getValue",
"outputs":[{"name":"","type":"string"}],
"type":"function"
}, {
"constant":false,
"inputs":[{"name":"value","type":"string"}],
"name":"setValue",
"outputs":[],
"type":"function"
}, {
"anonymous":false,
"inputs":[
{"indexed":false,"name":"oldValue","type":"string"},
{"indexed":false,"name":"newValue","type":"string"}
],
"name":"valueChanged",
"type":"event"
}
]
}
var contractInterface = new Wallet._Contract.Interface(contractABI.SimpleStorage);
var getValue = contractInterface.getValue()
var setValue = contractInterface.setValue("foobar");
var valueChanged = contractInterface.valueChanged()
test.equal(getValue.data, '0x20965255', "wrong call data");
test.equal(setValue.data, '0x93a0935200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006666f6f6261720000000000000000000000000000000000000000000000000000', "wrong transaction data");
test.ok(
(valueChanged.topics.length === 1 && valueChanged.topics[0] === '0x68ad6719a0070b3bb2f866fa0d46c8123b18cefe9b387ddb4feb6647ca418435'),
"wrong call data"
);
// @TODO - test decode
var privateKey = new Buffer(32);
privateKey.fill(0x42);
var wallet = new Wallet(privateKey, 'http://localhost:8545');
var contract = wallet.getContract(contractAddress, contractABI.SimpleStorage);
function testCall() {
return new Promise(function(resolve, reject) {
contract.getValue().then(function(result) {
test.equal(result, 'foobar', 'failed to call getVaue');
resolve(result);
}, function(error) {
test.ok(false, 'failed to call getValue (is parity running on this host?)');
reject(error);
});
});
}
function testEstimate() {
return new Promise(function(resolve, reject) {
contract.estimate.setValue('foo').then(function(result) {
test.equal(result.toString(16), '8b04', 'failed to estimate setVaue');
resolve(result);
}, function(error) {
test.ok(false, 'failed to call getValue (is parity running on this host?)');
reject(error);
});
});
}
Promise.all([
testCall(),
testEstimate(),
]).then(function(results) {
test.done();
}, function(error) {
console.log('ERROR', error);
test.done();
});
};
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,60 @@
'use strict';
var Wallet = require('../index.js');
// @TODO: Add testcases where format receives hexidecimal string
var BN = Wallet.utils.BN;
module.exports = function(test) {
function checkFormat(wei, targetEther, options) {
var ether = Wallet.formatEther(wei, options);
//console.log(wei, targetEther, options, ether);
test.equal(ether, targetEther, 'Failed to match formatted ether');
ether = ether.replace(/,/g, '');
test.ok(Wallet.parseEther(ether).eq(wei), 'Failed to convert back to wei');
}
function checkParse(ether, targetWei) {
//console.log(ether, targetWei, Wallet.parseEther(ether));
test.ok(targetWei.eq(Wallet.parseEther(ether)), 'Failed to match target wei');
}
checkParse('123.012345678901234567', new BN('123012345678901234567'));
checkParse('1.0', new BN('1000000000000000000'));
checkParse('1', new BN('1000000000000000000'));
checkParse('1.00', new BN('1000000000000000000'));
checkParse('01.0', new BN('1000000000000000000'));
checkParse('-1.0', new BN('-1000000000000000000'));
checkParse('0.1', new BN('100000000000000000'));
checkParse('.1', new BN('100000000000000000'));
checkParse('0.10', new BN('100000000000000000'));
checkParse('.100', new BN('100000000000000000'));
checkParse('00.100', new BN('100000000000000000'));
checkParse('-0.1', new BN('-100000000000000000'));
checkFormat(new BN('10000000000000000'), '0.01');
checkFormat(new BN('1000000000000000000'), '1.0');
checkFormat(new BN('1230000000000000000'), '1.23');
checkFormat(new BN('-1230000000000000000'), '-1.23');
checkFormat(new BN('1000000000000000000'), '1.000000000000000000', {pad: true});
checkFormat(new BN('123000000000000000000'), '123.000000000000000000', {pad: true});
checkFormat(new BN('1230000000000000000'), '1.230000000000000000', {pad: true});
checkFormat(new BN('-1230000000000000000'), '-1.230000000000000000', {pad: true});
checkFormat(new BN('1234567890000000000000000'), '1,234,567.89', {pad: false, commify: true});
checkFormat(new BN('1234567890000000000000000'), '1,234,567.890000000000000000', {pad: true, commify: true});
checkFormat(new BN('-1234567890000000000000000'), '-1,234,567.89', {pad: false, commify: true});
test.done();
}
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,34 @@
'use strict';
var Wallet = require('../index.js');
var ethereumUtil = require('ethereumjs-util');
var iban = require('../node_modules/web3/lib/web3/iban.js');
var utils = require('./utils.js');
module.exports = function(test) {
function testAddress(address) {
var officialIban = (iban.fromAddress(address))._iban;
var ethersAddress = Wallet.getAddress(officialIban);
var officialAddress = ethereumUtil.toChecksumAddress(address)
var ethersIban = Wallet.getIcapAddress(address);
test.equal(ethersAddress, officialAddress, 'wrong address');
test.equal(ethersIban, officialIban, 'wrong ICAP address');
}
test.expect(2 * (2 + 10000));
testAddress('0x0000000000000000000000000000000000000000');
testAddress('0xffffffffffffffffffffffffffffffffffffffff');
for (var i = 0; i < 10000; i++) {
testAddress(utils.randomHexString(20));
}
test.done();
};
module.exports.testSelf = module.exports;

19
tests/test-privatekey.js Normal file
View File

@@ -0,0 +1,19 @@
'use strict';
var Wallet = require('../index.js');
var ethereumUtil = require('ethereumjs-util');
var utils = require('./utils.js');
module.exports = function(test) {
for (var i = 0; i < 10000; i++) {
var privateKey = utils.randomBuffer(32);
var ethereumLib = '0x' + ethereumUtil.privateToAddress(privateKey).toString('hex');
var ethers = (new Wallet(privateKey)).address;
test.equal(ethers, ethereumUtil.toChecksumAddress(ethereumLib), 'wrong address');
}
test.done();
}
module.exports.testSelf = module.exports;

105
tests/test-providers.js Normal file
View File

@@ -0,0 +1,105 @@
'use strict';
var Wallet = require('../index.js');
var Web3 = require('web3');
// @TODO: We need to do a lot more test cases here:
// - homestead
// - sendTransaction
// - estimateGas with various parameters set and not set
// - estimateGas on a contract with from/value conditionals
// - Metamask-style injected Web3
module.exports = function(test) {
var url = 'http://localhost:8545';
var web3Provider = new Web3.providers.HttpProvider(url)
var web3 = new Web3(web3Provider)
var providers = [
(new Wallet.providers.Web3Provider(web3Provider)),
(new Wallet.providers.Web3Provider(web3)),
(new Wallet.providers.HttpProvider(url)),
(new Wallet.providers.EtherscanProvider({testnet: true})),
]
var pending = [];
function checkMethod(method, params, expectedValue) {
var checks = [];
providers.forEach(function(provider) {
checks.push(new Promise(function(resolve, reject) {
provider[method].apply(provider, params).then(function(value) {
resolve(value);
}, function(error) {
reject(error);
});
}));
});
pending.push(new Promise(function(resolve, reject) {
Promise.all(checks).then(function(results) {
if (!expectedValue) { expectedValue = results[0]; }
results.forEach(function(value) {
if (expectedValue instanceof Wallet.utils.BN) {
test.ok(expectedValue.eq(value), 'Failed ' + method);
} else if (typeof(expectedValue) === 'function') {
test.ok(expectedValue(value), 'Failed ' + method);
} else {
test.equal(value, expectedValue, 'Failed ' + method);
}
});
resolve();
}, function(error) {
console.log(error);
test.ok(false, 'Error - ' + error.message)
});
}));
}
checkMethod(
'getBalance', ['0x7357589f8e367c2C31F51242fB77B350A11830F3']
);
checkMethod(
'getTransactionCount', ['0x7357589f8e367c2C31F51242fB77B350A11830F3']
);
checkMethod('getGasPrice', []);
checkMethod(
'call', [{to: '0xdfaf84077cF4bCECA4F79d167F47041Ed3006D5b', data: '0x20965255'}],
'0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006666f6f6261720000000000000000000000000000000000000000000000000000'
);
checkMethod(
'estimateGas', [{
to: '0xdfaf84077cF4bCECA4F79d167F47041Ed3006D5b',
from: '0x7357589f8e367c2C31F51242fB77B350A11830F3',
data: '0x93a0935200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000'
}],
new Wallet.utils.BN('35588')
);
var privateKey = new Buffer(32);
privateKey.fill(0x42);
var wallet = new Wallet(privateKey, url);
pending.push(new Promise(function(resolve, reject) {
wallet.estimateGas({
to: '0xdfaf84077cF4bCECA4F79d167F47041Ed3006D5b',
data: '0x93a0935200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000'
}).then(function(value) {
test.equal(value.toString(10), '35588', 'Failed to call wallet.estimateGas');
resolve();
}, function(error) {
reject(error);
});
}));
Promise.all(pending).then(function(results) {
test.done();
}, function(error) {
console.log(error);
test.ok(false, 'Error occured: ' + error.message);
});
}
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,135 @@
'use strict'
var Wallet = require('../index.js');
var fs = require('fs');
module.exports = function(test) {
function equals(a, b) {
if (typeof(a) !== typeof(b)) { return false; }
if (Array.isArray(a)) {
if (!Array.isArray(b)) { return false; }
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) { return false; }
}
} else if (typeof(a) === 'object') {
if (!equals(Object.keys(a), Object.keys(b))) {
return false;
}
for (var key in a) {
if (!equals(a[key], b[key])) { return false; }
}
} else {
return (a === b);
}
return true;
}
var crowdsale = [
{
address: '0x2e326fA404Fc3661de4F4361776ed9bBABDC26E3',
data: fs.readFileSync('./test-wallets/wallet-test-encseed-foo.json').toString(),
password: 'foo',
privateKey: '0xcf367fc32bf789b3339c6664af4a12263e9db0e0eb70f247da1d1165e150c487',
type: 'crowdsale'
},
{
address: '0x0b88d4b324ec24C8c078551e6e5075547157E5b6',
data: fs.readFileSync('./test-wallets/wallet-test-encseed-no-password.json').toString(),
password: '',
privateKey: '0xd4375d2a931db84ea8825b69a3128913597744d9236cacec675cc18e1bda4446',
type: 'crowdsale'
}
];
var geth = [
{
address: '0x88a5C2d9919e46F883EB62F7b8Dd9d0CC45bc290',
data: fs.readFileSync('./test-wallets/wallet-test-geth-foo.json').toString(),
password: 'foo',
privateKey: '0xf03e581353c794928373fb0893bc731aefc4c4e234e643f3a46998b03cd4d7c5',
type: 'version3'
},
{
address: '0x4A9cf99357F5789251a8D7FaD5b86D0F31EEB938',
data: fs.readFileSync('./test-wallets/wallet-test-geth-no-password.json').toString(),
password: '',
privateKey: '0xa016182717223d01f776149ec0b4a217d0e9930cad263f205427c6d3cd5560e7',
type: 'version3'
},
];
// Test crowdsale private key decryption
crowdsale.forEach(function(testcase) {
// Check wallet type detection
test.ok(Wallet.isCrowdsaleWallet(testcase.data), 'wrong wallet type detected');
test.ok(!Wallet.isValidWallet(testcase.data), 'wrong wallet type detected');
var wallet = Wallet.decryptCrowdsale(testcase.data, testcase.password);
test.equal(wallet.privateKey, testcase.privateKey, 'wrong private key');
test.equal(wallet.address, testcase.address, 'wrong address');
});
var async = [];
geth.forEach(function(testcase) {
// Check wallet type detection
test.ok(Wallet.isValidWallet(testcase.data), 'wrong wallet type detected');
test.ok(!Wallet.isCrowdsaleWallet(testcase.data), 'wrong wallet type detected');
async.push(new Promise(function(resolve, reject) {
// Test private key decryption
var password = new Buffer(testcase.password, 'utf8');
Wallet.decrypt(testcase.data, password).then(function(wallet) {
test.equals(wallet.privateKey, testcase.privateKey, 'wrong private key')
test.equals(wallet.address, testcase.address, 'wrong address');
resolve();
}, function(error) {
console.log(error);
test.ok(false, 'callback returned error - ' + error.message);
reject(error);
});
}));
});
var privateKey = new Buffer(32);
privateKey.fill(0x42);
var password = new Buffer("foo", 'utf8');
async.push(new Promise(function(resolve, reject) {
(new Wallet(privateKey)).encrypt(password, {
scrypt: { N: (1 << 10), r: 4, p: 2 },
iv: '0xdeadbeef1deadbeef2deadbeef301234',
salt: '0xabcd1abcd2abcd3abcd4abcd5abcd6ef',
uuid: '0x01234567890123456789012345678901',
}).then(function(json) {
var jsonWallet = fs.readFileSync('./test-wallets/wallet-test-life.json').toString();
test.ok(equals(JSON.parse(json), JSON.parse(jsonWallet)), 'failed to encrypt wallet')
//test.equal(json, jsonWallet, 'failed to encrypt wallet');
Wallet.decrypt(json, password).then(function(wallet) {
test.equal(wallet.privateKey, '0x' + privateKey.toString('hex'), 'decryption failed');
resolve();
}, function(error) {
test.ok(false, 'callback returned error - ' + error.message);
reject(error);
});
}, function(error) {
test.ok(false, 'callback returned error - ' + error.message);
reject(error);
});
}));
Promise.all(async).then(function(results) {
test.done();
}, function(error) {
console.log(error);
test.done();
});
}
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,472 @@
'use strict';
var Wallet = require('../index.js');
var solc = require('solc');
var ethereumVm = require('ethereumjs-vm');
var ethereumUtil = require('ethereumjs-util');
var BN = Wallet.utils.BN;
var utils = require('./utils.js');
var random = utils.random;
// Create the indent given a tabstop
function indent(tabs) {
var indent = new Buffer(tabs * 4);
indent.fill(32);
return indent.toString('utf8')
}
/**
*
*
*/
function createContractOutput(types, values) {
var source = 'contract Test {\n';
source += ' function test() constant returns (' + types.join(', ') + ') {\n';
var returns = [];
for (var i = 0; i < types.length; i++) {
var name = String.fromCharCode(97 + i);
// Array type; do a deep copy
if (types[i].indexOf('[') >= 0) {
// Each count (or optionally empty) array type
var arrays = types[i].match(/\[[0-9]*\]/g);
// Allocate the space (only dynamic arrays require new)
source += indent(2) + types[i] + ' memory ' + name;
if (arrays[arrays.length - 1] === '[]') {
source += ' = new ' + types[i] + '(' + values[i].length+ ')';
}
source +=';\n';
var baseType = types[i].substring(0, types[i].indexOf('['));
function recursiveSet(item, indices) {
if (Array.isArray(item)) {
item.forEach(function(item, index) {
var i = indices.slice();
i.unshift(index);
recursiveSet(item, i);
});
} else {
var loc = '';
indices.forEach(function(index) {
loc = '[' + index + ']' + loc;
})
if (item instanceof BN) { item = item.toString(10); }
source += indent(2) + name + loc + ' = ' + baseType + '(' + item + ');\n';
}
}
recursiveSet(values[i], []);
// Dynamic type: bytes
} else if (types[i] === 'bytes') {
source += indent(2) + 'bytes memory ' + name + ' = new bytes(' + values[i].length + ');\n';
source += indent(2) + 'assembly {\n'
source += indent(3) + 'mstore(' + name + ', ' + values[i].length + ')\n';
for (var j = 0; j < values[i].length; j++) {
source += indent(3) + 'mstore8(add(' + name + ', ' + (32 + j) + '), ' + values[i][j] + ')\n';
}
source += indent(2) + '}\n'
/*
var value = '';
for (var j = 0; j < values[i].length; j++) {
value += '\\' + 'x' + values[i].slice(j, j + 1).toString('hex');
}
source += ' bytes memory ' + name + ' = "' + value + '";\n';
*/
// Dynamic type: string
} else if (types[i] === 'string') {
source += ' string memory ' + name + ' = "' + values[i] + '";\n';
// Static type; just use the stack
} else {
source += ' ' + types[i] + ' ' + name + ' = ' + types[i] + '(' + values[i] + ');\n';
}
// Track the name to return
returns.push(name);
}
// Return the values
source += ' return (' + returns.join(', ') + ');\n';
source += ' }\n';
source += '}\n';
try {
var contract = solc.compile(source, 0);
contract = contract.contracts.Test;
contract.sourceCode= source;
return contract;
} catch (error) {
console.log('Failed to compile ========');
console.log({types: types, values: values, contract: contract});
console.log(source);
console.log('========');
process.exit();
}
}
module.exports = function(test) {
function dumpHex(data) {
for (var i = 2; i < data.length; i += 64) {
console.log(' ' + data.substring(i, i + 64));
}
}
function recursiveEqual(a, b) {
function fail() {
return false;
}
if (typeof(a) === 'number') { a = new BN(a); }
if (typeof(b) === 'number') { b = new BN(b); }
if (utils.isHexString(a)) { a = utils.hexOrBuffer(a); }
if (utils.isHexString(b)) { b = utils.hexOrBuffer(b); }
if (a.eq) {
if (!b.eq || !a.eq(b)) { return fail(); }
return true;
}
if (Buffer.isBuffer(a)) {
if (!Buffer.isBuffer(b) || Buffer.compare(a, b) !== 0) { return fail(); }
return true;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) { return fail(); }
for (var i = 0; i < a.length; i++) {
if (!recursiveEqual(a[i], b[i])) { return fail(); }
}
return true;
}
if (a !== b) { return fail(); }
return true;
}
var checkPromises = [];
var nextTestId = 0;
var remaining = {};
function check(types, values, normalizedValues) {
if (!normalizedValues) { normalizedValues = values; }
// First make sure we agree with ourself
var ethersData = Wallet._Contract.Interface.encodeParams(types, values);
var ethersValues = Wallet._Contract.Interface.decodeParams(types, ethersData);
var okSelf = recursiveEqual(normalizedValues, ethersValues);
test.ok(okSelf, "self encode/decode failed");
if (!okSelf) {
console.log('okSelf', okSelf, types, values, normalizedValues, ethersValues);
}
checkPromises.push(new Promise(function(resolve, reject) {
var testId = (nextTestId++);
remaining[testId] = true;
// Use this when a contracts "hangs" (ie. 0-length arrays seem to hang the VM)
//console.log('a', testId, types, values);
try {
var contract = createContractOutput(types, values);
var contractInterface = new Wallet._Contract.Interface(JSON.parse(contract.interface));
var call = contractInterface.test.apply(contractInterface);
var vm = new ethereumVm();
vm.runCode({
code: new Buffer(contract.runtimeBytecode, 'hex'),
data: new Buffer(call.data.substring(2), 'hex'),
gasLimit: '0x80000000'
}, function(error, result) {
delete remaining[testId];
// Use this when contract hangs (see the above try)
//console.log('b', testId, Object.keys(remaining).join(','));
try {
var vmData = '0x' + result.return.toString('hex');
test.equal(ethersData, vmData, 'Failed to generate same output as VM');
if (ethersData !== vmData) {
console.log('\n\n');
console.log(contract.sourceCode);
console.log({
types: types,
values: values
});
console.log('ethers=');
dumpHex(ethersData);
console.log('vm=');
dumpHex('0x' + result.return.toString('hex'));
}
resolve();
} catch(error) {
reject(error);
}
});
} catch(error) {
console.log(error);
reject(error);
}
}));
}
// Test cases: https://github.com/ethereum/solidity.js/blob/master/test/coder.decodeParam.js
check(['int'], [new BN(1)]);
check(['int'], [new BN(16)]);
check(['int'], [new BN(-1)]);
check(['int256'], [new BN(1)]);
check(['int256'], [new BN(16)]);
check(['int256'], [new BN(-1)]);
check(['int8'], [new BN(16)]);
check(['int32'], [new BN(16)]);
check(['int64'], [new BN(16)]);
check(['int128'], [new BN(16)]);
check(['uint'], [new BN(1)]);
check(['uint'], [new BN(16)]);
check(['uint'], [new BN(-1)], [new BN('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)]);
check(['uint256'], [new BN(1)]);
check(['uint256'], [new BN(16)]);
check(['uint256'], [new BN(-1)], [new BN('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)]);
check(['uint8'], [new BN(16)]);
check(['uint32'], [new BN(16)]);
check(['uint64'], [new BN(16)]);
check(['uint128'], [new BN(16)]);
check(['int', 'int'], [new BN(1), new BN(2)]);
check(['int', 'int'], [new BN(1), new BN(2)]);
check(['int[2]', 'int'], [[new BN(12), new BN(22)], new BN(3)]);
check(['int[2]', 'int[]'], [[new BN(32), new BN(42)], [new BN(3), new BN(4), new BN(5)]]);
check(
['bytes32'],
['0x6761766f66796f726b0000000000000000000000000000000000000000000000']
);
check(
['bytes'],
[new Buffer('6761766f66796f726b', 'hex')]
);
check(
['string'],
['\uD835\uDF63']
);
check(
['address', 'string', 'bytes6[4]', 'int'],
[
"0x97916ef549947a3e0d321485a31dd2715a97d455",
"foobar2",
["0xa165ab0173c6", "0xf0f37bee9244", "0xc8dc0bf08d2b", "0xc8dc0bf08d2b"],
34
]
);
check(
['bytes32'],
['0x731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b']
);
check(
['bytes'],
[new Buffer('731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b')]
);
check(
['bytes32[2]'],
[['0x731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b',
'0x731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b']]
);
check(
['bytes'],
[new Buffer('131a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b' +
'231a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b' +
'331a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b')]
);
// Some extra checks for width and sign tests
check(['uint32'], [14], [new BN(14)]);
check(['uint32'], [14], [new BN(14)]);
check(['uint32'], [-14], [new BN(0xfffffff2)]);
check(['int32'], [14], [new BN(14)]);
check(['int32'], [-14], [new BN(-14)]);
check(['int8'], [new BN(1)], [new BN(1)]);
check(['int8'], [new BN(-1)], [new BN(-1)]);
check(['int8'], [new BN(189)], [new BN(-67)]);
check(['int8'], [new BN(-189)], [new BN(67)]);
check(['int8'], [new BN(257)], [new BN(1)]);
check(['uint8'], [new BN(343)], [new BN(87)]);
check(['uint8'], [new BN(-1)], [new BN(255)]);
check(['uint56[5]'], [[new BN(639), new BN(227), new BN(727), new BN(325), new BN(146)]]);
function randomTypeValue(onlyStatic) {
switch (random(0, (onlyStatic ? 5: 8))) {
case 0:
var size = random(1, 33);
return {
type: 'bytes' + size,
value: function() {
var value = '0x' + utils.randomBuffer(size).toString('hex');
return {
value: value,
normalized: value
}
}
}
case 1:
var signed = (random(0, 2) === 0);
var type = (!signed ? 'u': '') + 'int';
var size = 32;
if (random(0, 4) > 0) {
size = random(1, 33)
type += (8 * size);
}
return {
type: type,
value: function() {
var mask = '';
for (var i = 0; i < size; i++) { mask += 'ff'; }
var value = random(-500, 1000);
var normalized = (new BN(value)).toTwos(size * 8).and(new BN(mask, 16));
if (signed) {
normalized = normalized.fromTwos(size * 8);
}
return {
value: value,
normalized: normalized
};
}
}
case 2:
return {
type: 'address',
value: function() {
var value = '0x' + utils.randomBuffer(20).toString('hex');
return {
value: value,
normalized: value
};
}
}
case 3:
return {
type: 'bool',
value: function() {
var value = (random(0, 2) === 0);
return {
value: value,
normalized: value
};
}
}
case 4:
var size = random(1, 6); /// @TODO: Support random(0, 6)... Why is that even possible?
var subTypeValue = randomTypeValue(true);
return {
type: subTypeValue.type + '[' + size + ']',
value: function() {
var values = [];
var normalized = [];
for (var i = 0; i < size; i++) {
var value = subTypeValue.value();
values.push(value.value);
normalized.push(value.normalized);
}
return {
value: values,
normalized: normalized
};
}
}
case 5:
return {
type: 'bytes',
value: function() {
var value = utils.randomBuffer(random(0, 100));
return {
value: value,
normalized: value
};
},
skip: 0
}
case 6:
var text = 'abcdefghijklmnopqrstuvwxyz\u2014ABCDEFGHIJKLMNOPQRSTUVWXYZFOOBARfoobar'
return {
type: 'string',
value: function() {
var value = text.substring(0, random(0, 60));
return {
value: value,
normalized: value
};
}
}
case 7:
var size = random(1, 6); // @TODO: bug in solidity or VM prevents this from being 0
var subTypeValue = randomTypeValue(true);
return {
type: subTypeValue.type + '[]',
value: function() {
var values = [];
var normalized = [];
for (var i = 0; i < size; i++) {
var value = subTypeValue.value();
values.push(value.value);
normalized.push(value.normalized);
}
return {
value: values,
normalized: normalized
};
}
}
}
}
// @TODO: Test 0 arguments
// Create a bunch of random test cases
for (var i = 0; i < 1000; i++) {
var count = random(1, 4);
var types = [], values = [], normalized = [];;
for (var j = 0; j < count; j++) {
var type = randomTypeValue();
types.push(type.type);
var value = type.value();
values.push(value.value);
normalized.push(value.normalized);
}
check(types, values, normalized);
}
// Bug in solidity or in the VM, not sure, but this fails
// check(['uint8[4][]'], [ [] ]);
Promise.all(checkPromises).then(function(results) {
test.done();
}, function(error) {
console.log('ERROR', error);
test.done();
});
}
module.exports.testSelf = module.exports;

View File

@@ -0,0 +1,99 @@
'use strict';
var Wallet = require('../index.js');
var ethereumTx = require('ethereumjs-tx');
var utils = require('./utils.js');
function randomHexString(lowerRandomInterval, upperOpenInterval) {
return utils.randomHexString(utils.random(lowerRandomInterval, upperOpenInterval));
}
module.exports = function(test) {
function testTransaction(privateKey, transaction, signature) {
var rawTransaction = new ethereumTx(transaction);
rawTransaction.sign(privateKey);
var ethereumLib = '0x' + rawTransaction.serialize().toString('hex');
var wallet = new Wallet(privateKey);
var ethers = wallet.sign(transaction);
test.equal(ethers, ethereumLib, 'invalid transaction');
// @TODO: More testing on parsed transaction.
test.equal(wallet.address, Wallet.parseTransaction(ethers).from, 'invalid parseTransaction');
}
for (var i = 0; i < 10000; i++) {
var transaction = {
to: utils.randomHexString(20),
data: randomHexString(0, 10),
gasLimit: randomHexString(0, 10),
gasPrice: randomHexString(0, 10),
value: randomHexString(0, 10),
nonce: randomHexString(0, 10),
};
testTransaction(utils.randomBuffer(32), transaction);
}
// See: https://github.com/ethereumjs/ethereumjs-tx/blob/master/test/txs.json
testTransaction(new Buffer('164122e5d39e9814ca723a749253663bafb07f6af91704d9754c361eb315f0c1', 'hex'), {
nonce: "0x",
gasPrice: "0x09184e72a000",
gasLimit: "0x2710",
to: "0x0000000000000000000000000000000000000000",
value: "0x",
data: "0x7f7465737432000000000000000000000000000000000000000000000000000000600057",
}, {
v: "0x1c",
r: "0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab",
s: "0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13"
});
testTransaction(new Buffer('e0a462586887362a18a318b128dbc1e3a0cae6d4b0739f5d0419ec25114bc722', 'hex'), {
nonce: "0x06",
gasPrice: "0x09184e72a000",
gasLimit: "0x01f4",
to: "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c",
value: "0x016345785d8a0000",
data: "0x",
}, {
v: "0x1c",
r: "0x24a484bfa7380860e9fa0a9f5e4b64b985e860ca31abd36e66583f9030c2e29d",
s: "0x4d5ef07d9e73fa2fbfdad059591b4f13d0aa79e7634a2bb00174c9200cabb04d"
});
testTransaction(new Buffer('164122e5d39e9814ca723a749253663bafb07f6af91704d9754c361eb315f0c1', 'hex'), {
nonce: "0x06",
gasPrice: "0x09184e72a000",
gasLimit: "0x0974",
to: "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c",
value: "0x016345785d8a0000",
data: "0x00000000000000000000000000000000000000000000000000000000000000ad000000000000000000000000000000000000000000000000000000000000fafa0000000000000000000000000000000000000000000000000000000000000dfa0000000000000000000000000000000000000000000000000000000000000dfa00000000000000000000000000000000000000000000000000000000000000ad000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000df000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000df000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000d",
}, {
v: "0x1c",
r: "0x5e9361ca27e14f3af0e6b28466406ad8be026d3b0f2ae56e3c064043fb73ec77",
s: "0x29ae9893dac4f9afb1af743e25fbb6a63f7879a61437203cb48c997b0fcefc3a"
});
// Test all possible blank fields
var privateKey = new Buffer('0123456789012345678901234567890123456789012345678901234567890123', 'hex');
for (var i = 0; i < 64; i++) {
var transaction = {};
if (i & (1 << 0)) { transaction.nonce = '0x02'; }
if (i & (1 << 1)) { transaction.gasPrice = '0x03'; }
if (i & (1 << 2)) { transaction.gasLimit = '0x04'; }
if (i & (1 << 3)) { transaction.to = '0x0123456789012345678901234567890123456789'; }
if (i & (1 << 4)) { transaction.value = '0x05'; }
if (i & (1 << 5)) { transaction.data = '0x06'; }
testTransaction(privateKey, transaction);
}
test.done();
};
module.exports.testSelf = module.exports;

25
tests/utils.js Normal file
View File

@@ -0,0 +1,25 @@
var crypto = require('crypto');
var utils = require('../lib/utils.js');
function random(lowerRandomInterval, upperOpenInterval) {
return lowerRandomInterval + parseInt((upperOpenInterval - lowerRandomInterval) * Math.random());
}
function randomBuffer(length) {
var buffer = crypto.randomBytes(length);
return buffer;
}
function randomHexString(length) {
return '0x' + randomBuffer(length).toString('hex');
}
module.exports = {
random: random,
randomBuffer: randomBuffer,
randomHexString: randomHexString,
isHexString: utils.isHexString,
hexOrBuffer: utils.hexOrBuffer,
}