commit
8ab23c713d
@ -0,0 +1,61 @@ |
||||
const ObservableStore = require('obs-store') |
||||
const PendingBalanceCalculator = require('../lib/pending-balance-calculator') |
||||
const BN = require('ethereumjs-util').BN |
||||
|
||||
class BalanceController { |
||||
|
||||
constructor (opts = {}) { |
||||
const { address, accountTracker, txController, blockTracker } = opts |
||||
this.address = address |
||||
this.accountTracker = accountTracker |
||||
this.txController = txController |
||||
this.blockTracker = blockTracker |
||||
|
||||
const initState = { |
||||
ethBalance: undefined, |
||||
} |
||||
this.store = new ObservableStore(initState) |
||||
|
||||
this.balanceCalc = new PendingBalanceCalculator({ |
||||
getBalance: () => this._getBalance(), |
||||
getPendingTransactions: this._getPendingTransactions.bind(this), |
||||
}) |
||||
|
||||
this._registerUpdates() |
||||
} |
||||
|
||||
async updateBalance () { |
||||
const balance = await this.balanceCalc.getBalance() |
||||
this.store.updateState({ |
||||
ethBalance: balance, |
||||
}) |
||||
} |
||||
|
||||
_registerUpdates () { |
||||
const update = this.updateBalance.bind(this) |
||||
this.txController.on('submitted', update) |
||||
this.txController.on('confirmed', update) |
||||
this.txController.on('failed', update) |
||||
this.accountTracker.store.subscribe(update) |
||||
this.blockTracker.on('block', update) |
||||
} |
||||
|
||||
async _getBalance () { |
||||
const { accounts } = this.accountTracker.store.getState() |
||||
const entry = accounts[this.address] |
||||
const balance = entry.balance |
||||
return balance ? new BN(balance.substring(2), 16) : undefined |
||||
} |
||||
|
||||
async _getPendingTransactions () { |
||||
const pending = this.txController.getFilteredTxList({ |
||||
from: this.address, |
||||
status: 'submitted', |
||||
err: undefined, |
||||
}) |
||||
return pending |
||||
} |
||||
|
||||
} |
||||
|
||||
module.exports = BalanceController |
@ -0,0 +1,66 @@ |
||||
const ObservableStore = require('obs-store') |
||||
const extend = require('xtend') |
||||
const BalanceController = require('./balance') |
||||
|
||||
class ComputedbalancesController { |
||||
|
||||
constructor (opts = {}) { |
||||
const { accountTracker, txController, blockTracker } = opts |
||||
this.accountTracker = accountTracker |
||||
this.txController = txController |
||||
this.blockTracker = blockTracker |
||||
|
||||
const initState = extend({ |
||||
computedBalances: {}, |
||||
}, opts.initState) |
||||
this.store = new ObservableStore(initState) |
||||
this.balances = {} |
||||
|
||||
this._initBalanceUpdating() |
||||
} |
||||
|
||||
updateAllBalances () { |
||||
for (let address in this.accountTracker.store.getState().accounts) { |
||||
this.balances[address].updateBalance() |
||||
} |
||||
} |
||||
|
||||
_initBalanceUpdating () { |
||||
const store = this.accountTracker.store.getState() |
||||
this.addAnyAccountsFromStore(store) |
||||
this.accountTracker.store.subscribe(this.addAnyAccountsFromStore.bind(this)) |
||||
} |
||||
|
||||
addAnyAccountsFromStore(store) { |
||||
const balances = store.accounts |
||||
|
||||
for (let address in balances) { |
||||
this.trackAddressIfNotAlready(address) |
||||
} |
||||
} |
||||
|
||||
trackAddressIfNotAlready (address) { |
||||
const state = this.store.getState() |
||||
if (!(address in state.computedBalances)) { |
||||
this.trackAddress(address) |
||||
} |
||||
} |
||||
|
||||
trackAddress (address) { |
||||
let updater = new BalanceController({ |
||||
address, |
||||
accountTracker: this.accountTracker, |
||||
txController: this.txController, |
||||
blockTracker: this.blockTracker, |
||||
}) |
||||
updater.store.subscribe((accountBalance) => { |
||||
let newState = this.store.getState() |
||||
newState.computedBalances[address] = accountBalance |
||||
this.store.updateState(newState) |
||||
}) |
||||
this.balances[address] = updater |
||||
updater.updateBalance() |
||||
} |
||||
} |
||||
|
||||
module.exports = ComputedbalancesController |
@ -0,0 +1,15 @@ |
||||
// log rpc activity
|
||||
module.exports = createLoggerMiddleware |
||||
|
||||
function createLoggerMiddleware({ origin }) { |
||||
return function loggerMiddleware (req, res, next, end) { |
||||
next((cb) => { |
||||
if (res.error) { |
||||
log.error('Error in RPC response:\n', res) |
||||
} |
||||
if (req.isMetamaskInternal) return |
||||
log.info(`RPC (${origin}):`, req, '->', res) |
||||
cb() |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
// append dapp origin domain to request
|
||||
module.exports = createOriginMiddleware |
||||
|
||||
function createOriginMiddleware({ origin }) { |
||||
return function originMiddleware (req, res, next, end) { |
||||
req.origin = origin |
||||
next() |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
|
||||
module.exports = createProviderMiddleware |
||||
|
||||
// forward requests to provider
|
||||
function createProviderMiddleware({ provider }) { |
||||
return (req, res, next, end) => { |
||||
provider.sendAsync(req, (err, _res) => { |
||||
if (err) return end(err) |
||||
res.result = _res.result |
||||
end() |
||||
}) |
||||
} |
||||
} |
@ -1,48 +0,0 @@ |
||||
const through = require('through2') |
||||
|
||||
module.exports = ObjectMultiplex |
||||
|
||||
function ObjectMultiplex (opts) { |
||||
opts = opts || {} |
||||
// create multiplexer
|
||||
const mx = through.obj(function (chunk, enc, cb) { |
||||
const name = chunk.name |
||||
const data = chunk.data |
||||
if (!name) { |
||||
console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`) |
||||
return cb() |
||||
} |
||||
const substream = mx.streams[name] |
||||
if (!substream) { |
||||
console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`) |
||||
} else { |
||||
if (substream.push) substream.push(data) |
||||
} |
||||
return cb() |
||||
}) |
||||
mx.streams = {} |
||||
// create substreams
|
||||
mx.createStream = function (name) { |
||||
const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { |
||||
mx.push({ |
||||
name: name, |
||||
data: chunk, |
||||
}) |
||||
return cb() |
||||
}) |
||||
mx.on('end', function () { |
||||
return substream.emit('end') |
||||
}) |
||||
if (opts.error) { |
||||
mx.on('error', function () { |
||||
return substream.emit('error') |
||||
}) |
||||
} |
||||
return substream |
||||
} |
||||
// ignore streams (dont display orphaned data warning)
|
||||
mx.ignoreStream = function (name) { |
||||
mx.streams[name] = true |
||||
} |
||||
return mx |
||||
} |
@ -0,0 +1,51 @@ |
||||
const BN = require('ethereumjs-util').BN |
||||
const normalize = require('eth-sig-util').normalize |
||||
|
||||
class PendingBalanceCalculator { |
||||
|
||||
// Must be initialized with two functions:
|
||||
// getBalance => Returns a promise of a BN of the current balance in Wei
|
||||
// getPendingTransactions => Returns an array of TxMeta Objects,
|
||||
// which have txParams properties, which include value, gasPrice, and gas,
|
||||
// all in a base=16 hex format.
|
||||
constructor ({ getBalance, getPendingTransactions }) { |
||||
this.getPendingTransactions = getPendingTransactions |
||||
this.getNetworkBalance = getBalance |
||||
} |
||||
|
||||
async getBalance() { |
||||
const results = await Promise.all([ |
||||
this.getNetworkBalance(), |
||||
this.getPendingTransactions(), |
||||
]) |
||||
|
||||
const [ balance, pending ] = results |
||||
if (!balance) return undefined |
||||
|
||||
const pendingValue = pending.reduce((total, tx) => { |
||||
return total.add(this.calculateMaxCost(tx)) |
||||
}, new BN(0)) |
||||
|
||||
return `0x${balance.sub(pendingValue).toString(16)}` |
||||
} |
||||
|
||||
calculateMaxCost (tx) { |
||||
const txValue = tx.txParams.value |
||||
const value = this.hexToBn(txValue) |
||||
const gasPrice = this.hexToBn(tx.txParams.gasPrice) |
||||
|
||||
const gas = tx.txParams.gas |
||||
const gasLimit = tx.txParams.gasLimit |
||||
const gasLimitBn = this.hexToBn(gas || gasLimit) |
||||
|
||||
const gasCost = gasPrice.mul(gasLimitBn) |
||||
return value.add(gasCost) |
||||
} |
||||
|
||||
hexToBn (hex) { |
||||
return new BN(normalize(hex).substring(2), 16) |
||||
} |
||||
|
||||
} |
||||
|
||||
module.exports = PendingBalanceCalculator |
@ -1,10 +1,17 @@ |
||||
machine: |
||||
node: |
||||
version: 8.1.4 |
||||
dependencies: |
||||
pre: |
||||
- "npm i -g testem" |
||||
- "npm i -g mocha" |
||||
test: |
||||
override: |
||||
- "npm run ci" |
||||
- "npm test" |
||||
dependencies: |
||||
pre: |
||||
- sudo apt-get update |
||||
# get latest stable firefox |
||||
- sudo apt-get install firefox |
||||
- firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd |
||||
# get latest stable chrome |
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - |
||||
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' |
||||
- sudo apt-get update |
||||
- sudo apt-get install google-chrome-stable |
@ -0,0 +1,92 @@ |
||||
# Guide to Porting MetaMask to a New Environment |
||||
|
||||
MetaMask has been under continuous development for nearly two years now, and we’ve gradually discovered some very useful abstractions, that have allowed us to grow more easily. A couple of those layers together allow MetaMask to be ported to new environments and contexts increasingly easily. |
||||
|
||||
### The MetaMask Controller |
||||
|
||||
The core functionality of MetaMask all lives in what we call [The MetaMask Controller](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js). Our goal for this file is for it to eventually be its own javascript module that can be imported into any JS-compatible context, allowing it to fully manage an app's relationship to Ethereum. |
||||
|
||||
#### Constructor |
||||
|
||||
When calling `new MetaMask(opts)`, many platform-specific options are configured. The keys on `opts` are as follows: |
||||
|
||||
- initState: The last emitted state, used for restoring persistent state between sessions. |
||||
- platform: The `platform` object defines a variety of platform-specific functions, including opening the confirmation view, and opening web sites. |
||||
- encryptor - An object that provides access to the desired encryption methods. |
||||
|
||||
##### Encryptor |
||||
|
||||
An object that provides two simple methods, which can encrypt in any format you prefer. This parameter is optional, and will default to the browser-native WebCrypto API. |
||||
|
||||
- encrypt(password, object) - returns a Promise of a string that is ready for storage. |
||||
- decrypt(password, encryptedString) - Accepts the encrypted output of `encrypt` and returns a Promise of a restored `object` as it was encrypted. |
||||
|
||||
|
||||
##### Platform Options |
||||
|
||||
The `platform` object has a variety of options: |
||||
|
||||
- reload (function) - Will be called when MetaMask would like to reload its own context. |
||||
- openWindow ({ url }) - Will be called when MetaMask would like to open a web page. It will be passed a single `options` object with a `url` key, with a string value. |
||||
- getVersion() - Should return the current MetaMask version, as described in the current `CHANGELOG.md` or `app/manifest.json`. |
||||
|
||||
#### [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) |
||||
|
||||
This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated) |
||||
|
||||
#### [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335) |
||||
|
||||
Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works! |
||||
|
||||
### The UI |
||||
|
||||
The MetaMask UI is essentially just a website that can be configured by passing it the API and state subscriptions from above. Anyone could make a UI that consumes these, effectively reskinning MetaMask. |
||||
|
||||
You can see this in action in our file [ui/index.js](https://github.com/MetaMask/metamask-extension/blob/master/ui/index.js). There you can see an argument being passed in named `accountManager`, which is essentially a MetaMask controller (forgive its really outdated parameter name!). With access to that object, the UI is able to initialize a whole React/Redux app that relies on this API for its account/blockchain-related/persistent states. |
||||
|
||||
## Putting it Together |
||||
|
||||
As an example, a WebExtension is always defined by a `manifest.json` file. [In ours](https://github.com/MetaMask/metamask-extension/blob/master/app/manifest.json#L31), you can see that [background.js](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/background.js) is defined as a script to run in the background, and this is the file that we use to initialize the MetaMask controller. |
||||
|
||||
In that file, there's a lot going on, so it's maybe worth focusing on our MetaMask controller constructor to start. It looks something like this: |
||||
|
||||
```javascript |
||||
const controller = new MetamaskController({ |
||||
// User confirmation callbacks: |
||||
showUnconfirmedMessage: triggerUi, |
||||
unlockAccountMessage: triggerUi, |
||||
showUnapprovedTx: triggerUi, |
||||
// initial state |
||||
initState, |
||||
// platform specific api |
||||
platform, |
||||
}) |
||||
``` |
||||
Since `background.js` is essentially the Extension setup file, we can see it doing all the things specific to the extension platform: |
||||
- Defining how to open the UI for new messages, transactions, and even requests to unlock (reveal to the site) their account. |
||||
- Provide the instance's initial state, leaving MetaMask persistence to the platform. |
||||
- Providing a `platform` object. This is becoming our catch-all adapter for platforms to define a few other platform-variant features we require, like opening a web link. (Soon we will be moving encryption out here too, since our browser-encryption isn't portable enough!) |
||||
|
||||
## Ports, streams, and Web3! |
||||
|
||||
Everything so far has been enough to create a MetaMask wallet on virtually any platform that runs JS, but MetaMask's most unique feature isn't being a wallet, it's providing an Ethereum-enabled JavaScript context to websites. |
||||
|
||||
MetaMask has two kinds of [duplex stream APIs](https://github.com/substack/stream-handbook#duplex) that it exposes: |
||||
- [metamask.setupTrustedCommunication(connectionStream, originDomain)](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L352) - This stream is used to connect the user interface over a remote port, and may not be necessary for contexts where the interface and the metamask-controller share a process. |
||||
- [metamask.setupUntrustedCommunication(connectionStream, originDomain)](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L337) - This method is used to connect a new web site's web3 API to MetaMask's blockchain connection. Additionally, the `originDomain` is used to block detected phishing sites. |
||||
|
||||
### Web3 as a Stream |
||||
|
||||
If you are making a MetaMask-powered browser for a new platform, one of the trickiest tasks will be injecting the Web3 API into websites that are visited. On WebExtensions, we actually have to pipe data through a total of three JS contexts just to let sites talk to our background process (site -> contentscript -> background). |
||||
|
||||
To make this as easy as possible, we use one of our favorite internal tools, [web3-provider-engine](https://www.npmjs.com/package/web3-provider-engine) to construct a custom web3 provider object whose source of truth is a stream that we connect to remotely. |
||||
|
||||
To see how we do that, you can refer to the [inpage script](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/inpage.js) that we inject into every website. There you can see it creates a multiplex stream to the background, and uses it to initialize what we call the [inpage-provider](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/lib/inpage-provider.js), which you can see stubs a few methods out, but mostly just passes calls to `sendAsync` through the stream it's passed! That's really all the magic that's needed to create a web3-like API in a remote context, once you have a stream to MetaMask available. |
||||
|
||||
In `inpage.js` you can see we create a `PortStream`, that's just a class we use to wrap WebExtension ports as streams, so we can reuse our favorite stream abstraction over the more irregular API surface of the WebExtension. In a new platform, you will probably need to construct this stream differently. The key is that you need to construct a stream that talks from the site context to the background. Once you have that set up, it works like magic! |
||||
|
||||
If streams seem new and confusing to you, that's ok, they can seem strange at first. To help learn them, we highly recommend reading Substack's [Stream Handbook](https://github.com/substack/stream-handbook), or going through NodeSchool's interactive command-line class [Stream Adventure](https://github.com/workshopper/stream-adventure), also maintained by Substack. |
||||
|
||||
## Conclusion |
||||
|
||||
I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)! |
@ -1,21 +0,0 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width"> |
||||
<title>QUnit Example</title> |
||||
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.0.0.css"> |
||||
</head> |
||||
<body> |
||||
<div id="qunit"></div> |
||||
<div id="qunit-fixture"></div> |
||||
<script src="https://code.jquery.com/qunit/qunit-2.0.0.js"></script> |
||||
<script src="./jquery-3.1.0.min.js"></script> |
||||
<script src="./helpers.js"></script> |
||||
<script src="./test-bundle.js"></script> |
||||
<script src="/testem.js"></script> |
||||
|
||||
<div id="app-content"></div> |
||||
<script src="./bundle.js"></script> |
||||
</body> |
||||
</html> |
@ -1,119 +0,0 @@ |
||||
const PASSWORD = 'password123' |
||||
|
||||
QUnit.module('first time usage') |
||||
|
||||
QUnit.test('render init screen', function (assert) { |
||||
var done = assert.async() |
||||
let app |
||||
|
||||
wait(1000).then(function() { |
||||
app = $('#app-content').contents() |
||||
const recurseNotices = function () { |
||||
let button = app.find('button') |
||||
if (button.html() === 'Accept') { |
||||
let termsPage = app.find('.markdown')[0] |
||||
termsPage.scrollTop = termsPage.scrollHeight |
||||
return wait().then(() => { |
||||
button.click() |
||||
return wait() |
||||
}).then(() => { |
||||
return recurseNotices() |
||||
}) |
||||
} else { |
||||
return wait() |
||||
} |
||||
} |
||||
return recurseNotices() |
||||
}).then(function() { |
||||
// Scroll through terms
|
||||
var title = app.find('h1').text() |
||||
assert.equal(title, 'MetaMask', 'title screen') |
||||
|
||||
// enter password
|
||||
var pwBox = app.find('#password-box')[0] |
||||
var confBox = app.find('#password-box-confirm')[0] |
||||
pwBox.value = PASSWORD |
||||
confBox.value = PASSWORD |
||||
|
||||
return wait() |
||||
}).then(function() { |
||||
|
||||
// create vault
|
||||
var createButton = app.find('button.primary')[0] |
||||
createButton.click() |
||||
|
||||
return wait(1500) |
||||
}).then(function() { |
||||
|
||||
var created = app.find('h3')[0] |
||||
assert.equal(created.textContent, 'Vault Created', 'Vault created screen') |
||||
|
||||
// Agree button
|
||||
var button = app.find('button')[0] |
||||
assert.ok(button, 'button present') |
||||
button.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var detail = app.find('.account-detail-section')[0] |
||||
assert.ok(detail, 'Account detail section loaded.') |
||||
|
||||
var sandwich = app.find('.sandwich-expando')[0] |
||||
sandwich.click() |
||||
|
||||
return wait() |
||||
}).then(function() { |
||||
|
||||
var sandwich = app.find('.menu-droppo')[0] |
||||
var children = sandwich.children |
||||
var lock = children[children.length - 2] |
||||
assert.ok(lock, 'Lock menu item found') |
||||
lock.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var pwBox = app.find('#password-box')[0] |
||||
pwBox.value = PASSWORD |
||||
|
||||
var createButton = app.find('button.primary')[0] |
||||
createButton.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function() { |
||||
|
||||
var detail = app.find('.account-detail-section')[0] |
||||
assert.ok(detail, 'Account detail section loaded again.') |
||||
|
||||
return wait() |
||||
}).then(function (){ |
||||
|
||||
var qrButton = app.find('.fa.fa-qrcode')[0] |
||||
qrButton.click() |
||||
|
||||
return wait(1000) |
||||
}).then(function (){ |
||||
|
||||
var qrHeader = app.find('.qr-header')[0] |
||||
var qrContainer = app.find('#qr-container')[0] |
||||
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.') |
||||
assert.ok(qrContainer, 'QR Container found') |
||||
|
||||
return wait() |
||||
}).then(function (){ |
||||
|
||||
var networkMenu = app.find('.network-indicator')[0] |
||||
networkMenu.click() |
||||
|
||||
return wait() |
||||
}).then(function (){ |
||||
|
||||
var networkMenu = app.find('.network-indicator')[0] |
||||
var children = networkMenu.children |
||||
children.length[3] |
||||
assert.ok(children, 'All network options present') |
||||
|
||||
done() |
||||
}) |
||||
}) |
@ -0,0 +1,12 @@ |
||||
const Helper = require('./util/mascara-test-helper.js') |
||||
|
||||
window.addEventListener('load', () => { |
||||
window.METAMASK_SKIP_RELOAD = true |
||||
// inject app container
|
||||
const body = document.body |
||||
const container = document.createElement('div') |
||||
container.id = 'app-content' |
||||
body.appendChild(container) |
||||
// start ui
|
||||
require('../src/ui.js') |
||||
}) |
@ -1,13 +0,0 @@ |
||||
launch_in_dev: |
||||
- Chrome |
||||
- Firefox |
||||
- Opera |
||||
launch_in_ci: |
||||
- Chrome |
||||
- Firefox |
||||
- Opera |
||||
framework: |
||||
- qunit |
||||
before_tests: "npm run mascaraCi" |
||||
after_tests: "rm ./background.js ./test-bundle.js ./bundle.js" |
||||
test_page: "./index.html" |
@ -1,5 +0,0 @@ |
||||
const Helper = require('./util/mascara-test-helper.js') |
||||
|
||||
window.addEventListener('load', () => { |
||||
require('../src/ui.js') |
||||
}) |
@ -0,0 +1,59 @@ |
||||
// Karma configuration
|
||||
// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT)
|
||||
|
||||
module.exports = function(config) { |
||||
return { |
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: process.cwd(), |
||||
|
||||
browserConsoleLogOptions: { |
||||
terminal: false, |
||||
}, |
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['qunit'], |
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [ |
||||
'test/integration/jquery-3.1.0.min.js', |
||||
{ pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true }, |
||||
{ pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true }, |
||||
], |
||||
|
||||
proxies: { |
||||
'/images/': '/base/dist/chrome/images/', |
||||
'/fonts/': '/base/dist/chrome/fonts/', |
||||
}, |
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['progress'], |
||||
|
||||
// web server port
|
||||
port: 9876, |
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true, |
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO, |
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false, |
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['Chrome', 'Firefox'], |
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true, |
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
const getBaseConfig = require('./base.conf.js') |
||||
|
||||
module.exports = function(config) { |
||||
const settings = getBaseConfig(config) |
||||
settings.files.push('development/bundle.js') |
||||
settings.files.push('test/integration/bundle.js') |
||||
config.set(settings) |
||||
} |
@ -1,7 +0,0 @@ |
||||
function wait(time) { |
||||
return new Promise(function (resolve, reject) { |
||||
setTimeout(function () { |
||||
resolve() |
||||
}, time * 3 || 1500) |
||||
}) |
||||
} |
@ -0,0 +1,17 @@ |
||||
const getBaseConfig = require('./base.conf.js') |
||||
|
||||
module.exports = function(config) { |
||||
const settings = getBaseConfig(config) |
||||
|
||||
// ui and tests
|
||||
settings.files.push('dist/mascara/ui.js') |
||||
settings.files.push('dist/mascara/tests.js') |
||||
// service worker background
|
||||
settings.files.push({ pattern: 'dist/mascara/background.js', watched: false, included: false, served: true }), |
||||
settings.proxies['/background.js'] = '/base/dist/mascara/background.js' |
||||
|
||||
// use this to keep the browser open for debugging
|
||||
settings.browserNoActivityTimeout = 10000000 |
||||
|
||||
config.set(settings) |
||||
} |
@ -0,0 +1,93 @@ |
||||
const assert = require('assert') |
||||
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator') |
||||
const MockTxGen = require('../lib/mock-tx-gen') |
||||
const BN = require('ethereumjs-util').BN |
||||
let providerResultStub = {} |
||||
|
||||
const zeroBn = new BN(0) |
||||
const etherBn = new BN(String(1e18)) |
||||
const ether = '0x' + etherBn.toString(16) |
||||
|
||||
describe('PendingBalanceCalculator', function () { |
||||
let balanceCalculator |
||||
|
||||
describe('#calculateMaxCost(tx)', function () { |
||||
it('returns a BN for a given tx value', function () { |
||||
const txGen = new MockTxGen() |
||||
pendingTxs = txGen.generate({ |
||||
status: 'submitted', |
||||
txParams: { |
||||
value: ether, |
||||
gasPrice: '0x0', |
||||
gas: '0x0', |
||||
} |
||||
}, { count: 1 }) |
||||
|
||||
const balanceCalculator = generateBalanceCalcWith([], zeroBn) |
||||
const result = balanceCalculator.calculateMaxCost(pendingTxs[0]) |
||||
assert.equal(result.toString(), etherBn.toString(), 'computes one ether') |
||||
}) |
||||
|
||||
it('calculates gas costs as well', function () { |
||||
const txGen = new MockTxGen() |
||||
pendingTxs = txGen.generate({ |
||||
status: 'submitted', |
||||
txParams: { |
||||
value: '0x0', |
||||
gasPrice: '0x2', |
||||
gas: '0x3', |
||||
} |
||||
}, { count: 1 }) |
||||
|
||||
const balanceCalculator = generateBalanceCalcWith([], zeroBn) |
||||
const result = balanceCalculator.calculateMaxCost(pendingTxs[0]) |
||||
assert.equal(result.toString(), '6', 'computes 6 wei of gas') |
||||
}) |
||||
}) |
||||
|
||||
describe('if you have no pending txs and one ether', function () { |
||||
|
||||
beforeEach(function () { |
||||
balanceCalculator = generateBalanceCalcWith([], etherBn) |
||||
}) |
||||
|
||||
it('returns the network balance', async function () { |
||||
const result = await balanceCalculator.getBalance() |
||||
assert.equal(result, ether, `gave ${result} needed ${ether}`) |
||||
}) |
||||
}) |
||||
|
||||
describe('if you have a one ether pending tx and one ether', function () { |
||||
beforeEach(function () { |
||||
const txGen = new MockTxGen() |
||||
pendingTxs = txGen.generate({ |
||||
status: 'submitted', |
||||
txParams: { |
||||
value: ether, |
||||
gasPrice: '0x0', |
||||
gas: '0x0', |
||||
} |
||||
}, { count: 1 }) |
||||
|
||||
balanceCalculator = generateBalanceCalcWith(pendingTxs, etherBn) |
||||
}) |
||||
|
||||
it('returns the subtracted result', async function () { |
||||
const result = await balanceCalculator.getBalance() |
||||
assert.equal(result, '0x0', `gave ${result} needed '0x0'`) |
||||
return true |
||||
}) |
||||
|
||||
}) |
||||
}) |
||||
|
||||
function generateBalanceCalcWith (transactions, providerStub = zeroBn) { |
||||
const getPendingTransactions = async () => transactions |
||||
const getBalance = async () => providerStub |
||||
|
||||
return new PendingBalanceCalculator({ |
||||
getBalance, |
||||
getPendingTransactions, |
||||
}) |
||||
} |
||||
|
@ -1,10 +0,0 @@ |
||||
launch_in_dev: |
||||
- Chrome |
||||
- Firefox |
||||
launch_in_ci: |
||||
- Chrome |
||||
- Firefox |
||||
framework: |
||||
- qunit |
||||
before_tests: "npm run buildCiUnits" |
||||
test_page: "test/integration/index.html" |
Loading…
Reference in new issue