1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #4919 from MetaMask/refactor-tx-list

Refactor and Redesign Transaction List
This commit is contained in:
Dan Finlay 2018-08-27 15:02:07 -07:00 committed by GitHub
commit 4b17ec67ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 8102 additions and 8116 deletions

View File

@ -451,6 +451,9 @@
"hideTokenPrompt": {
"message": "Hide Token?"
},
"history": {
"message": "History"
},
"howToDeposit": {
"message": "How would you like to deposit Ether?"
},
@ -651,7 +654,7 @@
"message": "No transaction history."
},
"noTransactions": {
"message": "No Transactions"
"message": "You have no transactions"
},
"notFound": {
"message": "Not Found"
@ -702,6 +705,9 @@
"pasteSeed": {
"message": "Paste your seed phrase here!"
},
"pending": {
"message": "pending"
},
"personalAddressDetected": {
"message": "Personal address detected. Input the token contract address."
},
@ -730,6 +736,9 @@
"qrCode": {
"message": "Show QR Code"
},
"queue": {
"message": "Queue"
},
"readdToken": {
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu."
},
@ -897,6 +906,12 @@
"sendTokens": {
"message": "Send Tokens"
},
"sentEther": {
"message": "sent ether"
},
"sentTokens": {
"message": "sent tokens"
},
"separateEachWord": {
"message": "Separate each word with a single space"
},
@ -910,6 +925,9 @@
"orderOneHere": {
"message": "Order a Trezor or Ledger and keep your funds in cold storage"
},
"outgoing": {
"message": "Outgoing"
},
"searchTokens": {
"message": "Search Tokens"
},
@ -973,6 +991,9 @@
"sign": {
"message": "Sign"
},
"signatureRequest": {
"message": "Signature Request"
},
"signed": {
"message": "Signed"
},

12252
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -189,6 +189,7 @@
"react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0",
"react-markdown": "^3.0.0",
"react-media": "^1.8.0",
"react-redux": "^5.0.5",
"react-router-dom": "^4.2.2",
"react-select": "^1.0.0",

View File

@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value'))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH')
assert.equal(await txValues[0].getText(), '-1 ETH')
})
})

View File

@ -225,19 +225,9 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
}
await clickWordAndWait(words[0])
await clickWordAndWait(words[1])
await clickWordAndWait(words[2])
await clickWordAndWait(words[3])
await clickWordAndWait(words[4])
await clickWordAndWait(words[5])
await clickWordAndWait(words[6])
await clickWordAndWait(words[7])
await clickWordAndWait(words[8])
await clickWordAndWait(words[9])
await clickWordAndWait(words[10])
await clickWordAndWait(words[11])
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
@ -414,12 +404,12 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
}
})
})
@ -457,14 +447,11 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
})
})
@ -487,9 +474,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
await txListItem.click()
await delay(regularDelayMs)
await delay(largeDelayMs)
})
it('displays the contract creation data', async () => {
@ -511,13 +498,15 @@ describe('MetaMask', function () {
it('confirms a deploy contract transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await delay(largeDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
await delay(regularDelayMs)
})
@ -538,9 +527,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -568,15 +557,17 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 4
}, 10000)
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
const firstTxAddress = await txAccounts[0].getText()
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
// const firstTxAddress = await txAccounts[0].getText()
// assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
})
it('calls and confirms a contract method where ETH is received', async () => {
@ -590,7 +581,7 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const txListItem = await findElement(driver, By.css('.tx-list-item'))
const txListItem = await findElement(driver, By.css('.transaction-list-item'))
await txListItem.click()
await delay(regularDelayMs)
@ -598,18 +589,20 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 5
}, 10000)
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
})
it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
@ -654,12 +647,11 @@ describe('MetaMask', function () {
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
await delay(largeDelayMs)
})
it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
await addToken.click()
await delay(regularDelayMs)
})
@ -683,7 +675,7 @@ describe('MetaMask', function () {
})
it('renders the balance for the new token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
const tokenAmount = await balance.getText()
assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
@ -752,21 +744,25 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value'))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
}
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
assert.equal(await tx.getText(), 'Confirmed')
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000)
assert.equal(await tx.getText(), 'Sent Tokens')
})
})
@ -789,9 +785,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
await findElements(driver, By.css('.transaction-list__pending-transactions'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -838,25 +834,28 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 2)
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 2
}, 10000)
const txValues = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
await walletBalance.click()
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
await tokenListItems[0].click()
await delay(regularDelayMs)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
assert.equal(await tokenBalanceAmount.getText(), '43')
const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
assert.equal(await tokenBalanceAmount.getText(), '43 TST')
}
})
})
@ -880,9 +879,14 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
driver.wait(async () => {
const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item'))
return pendingTxes.length === 1
}, 10000)
const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
await txListItem.click()
await delay(regularDelayMs)
})
@ -953,10 +957,15 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const txValues = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
})
@ -1006,7 +1015,7 @@ describe('MetaMask', function () {
})
it('renders the balance for the chosen token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
await delay(regularDelayMs)
})
@ -1071,4 +1080,4 @@ describe('MetaMask', function () {
}
})
})
})
})

View File

@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) {
$('button.btn-primary.btn--large')[0].click()
// Verify added token image
let heroBalance = await queryAsync($, '.hero-balance')
let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) {
// $('button.btn-primary--lg')[0].click()
// Verify added token image
heroBalance = await queryAsync($, '.hero-balance')
heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(heroBalance.find('.identicon')[0], 'token added')
}

View File

@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
const pendingRequestItem = $.find('.transaction-list-item')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()

View File

@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) {
await timeout(1000)
reactTriggerChange(selectState[0])
await timeout(1000)
const txView = await queryAsync($, '.tx-view')
const heroBalance = await findAsync($(txView), '.hero-balance')
const fiatAmount = await findAsync($(heroBalance), '.fiat-amount')
assert.equal(fiatAmount[0].textContent, '₱102,707.97')
const txView = await queryAsync($, '.transaction-view')
const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
}

View File

@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) {
selectState.val('send new ui')
reactTriggerChange(selectState[0])
const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button')
const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button')
assert.ok(sendScreenButton[1], 'send screen button present')
sendScreenButton[1].click()

View File

@ -29,26 +29,23 @@ async function runTxListItemsTest (assert, done) {
assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click()
const txListItems = await queryAsync($, '.tx-list-item')
const txListItems = await queryAsync($, '.transaction-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
const unapprovedTx = txListItems[0]
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
const retryTx = txListItems[1]
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span')
const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry')
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
const unapprovedMsg = txListItems[3]
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const failedTx = txListItems[4]
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
const shapeShiftTx = txListItems[5]
@ -56,10 +53,10 @@ async function runTxListItemsTest (assert, done) {
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
const confirmedTokenTx = txListItems[6]
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address')
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
const rejectedTx = txListItems[7]
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
}

View File

@ -1,33 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
// Main Views
const TxView = require('./components/tx-view')
const WalletView = require('./components/wallet-view')
module.exports = AccountAndTransactionDetails
inherits(AccountAndTransactionDetails, Component)
function AccountAndTransactionDetails () {
Component.call(this)
}
AccountAndTransactionDetails.prototype.render = function () {
return h('div.account-and-transaction-details', [
// wallet
h(WalletView, {
style: {
},
responsiveDisplayClassname: '.lap-visible',
}, [
]),
// transaction
h(TxView, {
style: {
},
}, [
]),
])
}

View File

@ -18,7 +18,7 @@ const ConfirmTransaction = require('./components/pages/confirm-transaction')
const WalletView = require('./components/wallet-view')
// other views
const Home = require('./components/pages/home')
import Home from './components/pages/home'
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
@ -182,7 +182,7 @@ class App extends Component {
}, [
// A second instance of Walletview is used for non-mobile viewports
this.props.sidebarOpen ? h(WalletView, {
responsiveDisplayClassname: '.sidebar',
responsiveDisplayClassname: 'sidebar',
style: {},
}) : undefined,

View File

@ -4,8 +4,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
import CurrencyDisplay from './currency-display'
const { formatBalance, generateBalanceObject } = require('../util')
@ -80,38 +79,12 @@ BalanceComponent.prototype.renderBalance = function () {
style: {},
}, this.getTokenBalance(formattedBalance, shorten)),
showFiat ? this.renderFiatValue(formattedBalance) : null,
showFiat && h(CurrencyDisplay, {
value: balanceValue,
}),
])
}
BalanceComponent.prototype.renderFiatValue = function (formattedBalance) {
const { conversionRate, currentCurrency } = this.props
const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate)
const fiatPrefix = currentCurrency === 'USD' ? '$' : ''
return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix)
}
BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) {
const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0
if (shouldNotRenderFiat) return null
const upperCaseFiatSuffix = fiatSuffix.toUpperCase()
const display = currencies.find(currency => currency.code === upperCaseFiatSuffix)
? currencyFormatter.format(Number(fiatDisplayNumber), {
code: upperCaseFiatSuffix,
})
: `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}`
return h('div.fiat-amount', {
style: {},
}, display)
}
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)

View File

@ -1,267 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../actions')
const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading-screen')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
BuyButtonSubview.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps)(BuyButtonSubview)
function mapStateToProps (state) {
return {
identity: state.appState.identity,
account: state.metamask.accounts[state.appState.buyView.buyAddress],
warning: state.appState.warning,
buyView: state.appState.buyView,
network: state.metamask.network,
provider: state.metamask.provider,
context: state.appState.currentView.context,
isSubLoading: state.appState.isSubLoading,
}
}
inherits(BuyButtonSubview, Component)
function BuyButtonSubview () {
Component.call(this)
}
BuyButtonSubview.prototype.render = function () {
return (
h('div', {
style: {
width: '100%',
},
}, [
this.headerSubview(),
this.primarySubview(),
])
)
}
BuyButtonSubview.prototype.headerSubview = function () {
const props = this.props
const isLoading = props.isSubLoading
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
// header bar (back button, label)
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: this.backButtonContext.bind(this),
style: {
position: 'absolute',
left: '10px',
},
}),
h('h2.text-transform-uppercase.flex-center', {
style: {
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, this.context.t('depositEth')),
]),
// loading indication
h('div', {
style: {
position: 'absolute',
top: '57vh',
left: '49vw',
},
}, [
isLoading && h(Loading),
]),
// account panel
h('div', {
style: {
width: '80%',
},
}, [
h(AccountPanel, {
showFullAddress: true,
identity: props.identity,
account: props.account,
}),
]),
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('h3.text-transform-uppercase.flex-center', {
style: {
paddingLeft: '15px',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, this.context.t('selectService')),
]),
])
)
}
BuyButtonSubview.prototype.primarySubview = function () {
const props = this.props
const network = props.network
switch (network) {
case 'loading':
return
case '1':
return this.mainnetSubview()
// Ropsten, Rinkeby, Kovan
case '3':
case '4':
case '42':
const networkName = getNetworkDisplayName(network)
const label = `${networkName} ${this.context.t('testFaucet')}`
return (
h('div.flex-column', {
style: {
alignItems: 'center',
margin: '20px 50px',
},
}, [
h('button.text-transform-uppercase', {
onClick: () => this.props.dispatch(actions.buyEth({ network })),
style: {
marginTop: '15px',
},
}, label),
// Kovan only: Dharma loans beta
network === '42' ? (
h('button.text-transform-uppercase', {
onClick: () => this.navigateTo('https://borrow.dharma.io/'),
style: {
marginTop: '15px',
},
}, this.context.t('borrowDharma'))
) : null,
])
)
default:
return (
h('h2.error', this.context.t('unknownNetworkId'))
)
}
}
BuyButtonSubview.prototype.mainnetSubview = function () {
const props = this.props
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
h('.flex-row.selected-exchange', {
style: {
position: 'relative',
right: '35px',
marginTop: '20px',
marginBottom: '20px',
},
}, [
h(RadioList, {
defaultFocus: props.buyView.subview,
labels: [
'Coinbase',
'ShapeShift',
],
subtext: {
'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`,
'ShapeShift': this.context.t('crypto'),
},
onClick: this.radioHandler.bind(this),
}),
]),
h('h3.text-transform-uppercase', {
style: {
paddingLeft: '15px',
fontFamily: 'Montserrat Light',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, props.buyView.subview),
this.formVersionSubview(),
])
)
}
BuyButtonSubview.prototype.formVersionSubview = function () {
const network = this.props.network
if (network === '1') {
if (this.props.buyView.formView.coinbase) {
return h(CoinbaseForm, this.props)
} else if (this.props.buyView.formView.shapeshift) {
return h(ShapeshiftForm, this.props)
}
}
}
BuyButtonSubview.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}
BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage({transForward: false}))
} else {
this.props.dispatch(actions.goHome())
}
}
BuyButtonSubview.prototype.radioHandler = function (event) {
switch (event.target.title) {
case 'Coinbase':
return this.props.dispatch(actions.coinBaseSubview())
case 'ShapeShift':
return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
}
}

View File

@ -0,0 +1,26 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { ETH } from '../../constants/common'
export default class CurrencyDisplay extends PureComponent {
static propTypes = {
className: PropTypes.string,
displayValue: PropTypes.string,
prefix: PropTypes.string,
currency: PropTypes.oneOf([ETH]),
}
render () {
const { className, displayValue, prefix } = this.props
const text = `${prefix || ''}${displayValue}`
return (
<div
className={className}
title={text}
>
{ text }
</div>
)
}
}

View File

@ -0,0 +1,19 @@
import { connect } from 'react-redux'
import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
const mapStateToProps = (state, ownProps) => {
const { value, numberOfDecimals = 2, currency } = ownProps
const { metamask: { currentCurrency, conversionRate } } = state
const toCurrency = currency || currentCurrency
const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
const formattedValue = formatCurrency(convertedValue, toCurrency)
const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
return {
displayValue,
}
}
export default connect(mapStateToProps)(CurrencyDisplay)

View File

@ -0,0 +1 @@
export { default } from './currency-display.container'

View File

@ -0,0 +1,27 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import CurrencyDisplay from '../currency-display.component'
describe('CurrencyDisplay Component', () => {
it('should render text with a className', () => {
const wrapper = shallow(<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
/>)
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '$123.45')
})
it('should render text with a prefix', () => {
const wrapper = shallow(<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
prefix="-"
/>)
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '-$123.45')
})
})

View File

@ -0,0 +1,61 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../currency-display.container.js', {
'react-redux': {
connect: ms => {
mapStateToProps = ms
return () => ({})
},
},
})
describe('CurrencyDisplay container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
},
}
const tests = [
{
props: {
value: '0x2386f26fc10000',
numberOfDecimals: 2,
currency: 'usd',
},
result: {
displayValue: '$2.80 USD',
},
},
{
props: {
value: '0x2386f26fc10000',
},
result: {
displayValue: '$2.80 USD',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
numberOfDecimals: 3,
},
result: {
displayValue: '1.266 ETH',
},
},
]
tests.forEach(({ props, result }) => {
assert.deepEqual(mapStateToProps(mockState, props), result)
})
})
})
})

View File

@ -1,60 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = RadioList
inherits(RadioList, Component)
function RadioList () {
Component.call(this)
}
RadioList.prototype.render = function () {
const props = this.props
const activeClass = '.custom-radio-selected'
const inactiveClass = '.custom-radio-inactive'
const {
labels,
defaultFocus,
} = props
return (
h('.flex-row', {
style: {
fontSize: '12px',
},
}, [
h('.flex-column.custom-radios', {
style: {
marginRight: '5px',
},
},
labels.map((lable, i) => {
let isSelcted = (this.state !== null)
isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable)
return h(isSelcted ? activeClass : inactiveClass, {
title: lable,
onClick: (event) => {
this.setState({selected: event.target.title})
props.onClick(event)
},
})
})
),
h('.text', {},
labels.map((lable) => {
if (props.subtext) {
return h('.flex-row', {}, [
h('.radio-titles', lable),
h('.radio-titles-subtext', `- ${props.subtext[lable]}`),
])
} else {
return h('.radio-titles', lable)
}
})
),
])
)
}

View File

@ -47,7 +47,8 @@ IdenticonComponent.prototype.render = function () {
})
)
: (
h('img.balance-icon', {
h('img', {
className: `${className} balance-icon`,
src: './images/eth_logo.svg',
style: {
height: diameter,

View File

@ -1,23 +1,35 @@
@import './app-header/index';
@import './button-group/index';
@import './confirm-page-container/index';
@import './export-text-container/index';
@import './selected-account/index';
@import './info-box/index';
@import './network-display/index';
@import './menu-bar/index';
@import './confirm-page-container/index';
@import './modals/index';
@import './network-display/index';
@import './page-container/index';
@import './pages/index';
@import './modals/index';
@import './selected-account/index';
@import './sender-to-recipient/index';
@import './tabs/index';
@import './app-header/index';
@import './transaction-view/index';
@import './transaction-view-balance/index';
@import './transaction-list/index';
@import './transaction-list-item/index';
@import './transaction-status/index';

View File

@ -0,0 +1 @@
export { default } from './menu-bar.container'

View File

@ -0,0 +1,23 @@
.menu-bar {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex: 0 0 auto;
margin-bottom: 16px;
padding: 5px;
border-bottom: 1px solid #e5e5e5;
&__sidebar-button {
font-size: 1.25rem;
cursor: pointer;
padding: 10px;
}
&__open-in-browser {
cursor: pointer;
display: flex;
justify-content: center;
padding: 10px;
}
}

View File

@ -0,0 +1,52 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Tooltip from '../tooltip'
import SelectedAccount from '../selected-account'
export default class MenuBar extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
sidebarOpen: PropTypes.bool,
showSidebar: PropTypes.func,
}
render () {
const { t } = this.context
const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props
return (
<div className="menu-bar">
<Tooltip
title={t('menu')}
position="bottom"
>
<div
className="fa fa-bars menu-bar__sidebar-button"
onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
/>
</Tooltip>
<SelectedAccount />
{
!isMascara && (
<Tooltip
title={t('openInTab')}
position="bottom"
>
<div
className="menu-bar__open-in-browser"
onClick={() => global.platform.openExtensionInBrowser()}
>
<img src="images/popout.svg" />
</div>
</Tooltip>
)
}
</div>
)
}
}

View File

@ -0,0 +1,21 @@
import { connect } from 'react-redux'
import MenuBar from './menu-bar.component'
import { showSidebar, hideSidebar } from '../../actions'
const mapStateToProps = state => {
const { appState: { sidebarOpen, isMascara } } = state
return {
sidebarOpen,
isMascara,
}
}
const mapDispatchToProps = dispatch => {
return {
showSidebar: () => dispatch(showSidebar()),
hideSidebar: () => dispatch(hideSidebar()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)

View File

@ -2,8 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
import Button from '../../button'
import Identicon from '../../../components/identicon'
import TokenBalance from './token-balance'
import Identicon from '../../identicon'
import TokenBalance from '../../token-balance'
export default class ConfirmAddToken extends Component {
static contextTypes = {

View File

@ -1,2 +0,0 @@
import TokenBalance from './token-balance.container'
module.exports = TokenBalance

View File

@ -1,16 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class TokenBalance extends Component {
static propTypes = {
string: PropTypes.string,
symbol: PropTypes.string,
error: PropTypes.string,
}
render () {
return (
<div className="hide-text-overflow">{ this.props.string }</div>
)
}
}

View File

@ -12,12 +12,12 @@ import {
CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH,
} from '../../../routes'
import { isConfirmDeployContract } from './confirm-transaction-switch.util'
import { isConfirmDeployContract } from '../../../helpers/transactions.util'
import {
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM,
} from './confirm-transaction-switch.constants'
} from '../../../constants/transactions'
export default class ConfirmTransactionSwitch extends Component {
static propTypes = {

View File

@ -1,3 +0,0 @@
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'

View File

@ -1,239 +0,0 @@
const { Component } = require('react')
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const { Redirect, withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript')
const actions = require('../../actions')
const log = require('loglevel')
// init
const NewKeyChainScreen = require('../../new-keychain')
// mascara
const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
// accounts
const MainContainer = require('../../main-container')
// other views
const BuyView = require('../../components/buy-button-subview')
const QrView = require('../../components/qr-code')
// Routes
const {
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
} = require('../../routes')
const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
class Home extends Component {
componentDidMount () {
const {
history,
unconfirmedTransactionsCount = 0,
} = this.props
// unapprovedTxs and unapproved messages
if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
render () {
log.debug('rendering primary')
const {
noActiveNotices,
lostAccounts,
forgottenPassword,
currentView,
activeAddress,
seedWords,
} = this.props
// notices
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
return h(Redirect, {
to: {
pathname: NOTICE_ROUTE,
},
})
}
// seed words
if (seedWords) {
log.debug('rendering seed words')
return h(Redirect, {
to: {
pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
},
})
}
if (forgottenPassword) {
log.debug('rendering restore vault screen')
return h(Redirect, {
to: {
pathname: RESTORE_VAULT_ROUTE,
},
})
}
// show current view
switch (currentView.name) {
case 'accountDetail':
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
case 'onboardingBuyEth':
log.debug('rendering onboarding buy ether screen')
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
case 'qr':
log.debug('rendering show qr screen')
return h('div', {
style: {
position: 'absolute',
height: '100%',
top: '0px',
left: '0px',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr'}),
]),
])
default:
log.debug('rendering default, account detail screen')
return h(MainContainer, {key: 'account-detail'})
}
}
}
Home.propTypes = {
currentCurrency: PropTypes.string,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
isMascara: PropTypes.bool,
isOnboarding: PropTypes.bool,
isUnlocked: PropTypes.bool,
networkDropdownOpen: PropTypes.bool,
history: PropTypes.object,
dispatch: PropTypes.func,
selectedAddress: PropTypes.string,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
isInitialized: PropTypes.bool,
forgottenPassword: PropTypes.bool,
activeAddress: PropTypes.string,
unapprovedTxs: PropTypes.object,
seedWords: PropTypes.string,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
welcomeScreenSeen: PropTypes.bool,
isPopup: PropTypes.bool,
isMouseUser: PropTypes.bool,
t: PropTypes.func,
unconfirmedTransactionsCount: PropTypes.number,
}
function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
} = appState
const {
accounts,
address,
isInitialized,
noActiveNotices,
seedWords,
unapprovedTxs,
nextUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
} = metamask
const selected = address || Object.keys(accounts)[0]
return {
// state from plugin
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
noActiveNotices,
isInitialized,
isUnlocked: state.metamask.isUnlocked,
selectedAddress: state.metamask.selectedAddress,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
isPopup: state.metamask.isPopup,
seedWords: state.metamask.seedWords,
unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
menuOpen: state.appState.menuOpen,
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
nextUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency,
isMouseUser: state.appState.isMouseUser,
isRevealingSeedWords: state.metamask.isRevealingSeedWords,
Qr: state.appState.Qr,
welcomeScreenSeen: state.metamask.welcomeScreenSeen,
// state needed to get account dropdown temporarily rendering from app bar
selected,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}
module.exports = compose(
withRouter,
connect(mapStateToProps)
)(Home)

View File

@ -0,0 +1,66 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Media from 'react-media'
import { Redirect } from 'react-router-dom'
import WalletView from '../../wallet-view'
import TransactionView from '../../transaction-view'
import {
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
} from '../../../routes'
export default class Home extends PureComponent {
static propTypes = {
history: PropTypes.object,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
forgottenPassword: PropTypes.bool,
seedWords: PropTypes.string,
unconfirmedTransactionsCount: PropTypes.number,
}
componentDidMount () {
const { history, unconfirmedTransactionsCount = 0 } = this.props
if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
render () {
const {
noActiveNotices,
lostAccounts,
forgottenPassword,
seedWords,
} = this.props
// notices
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
return <Redirect to={{ pathname: NOTICE_ROUTE }} />
}
// seed words
if (seedWords) {
return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
}
if (forgottenPassword) {
return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
}
return (
<div className="main-container">
<div className="account-and-transaction-details">
<Media
query="(min-width: 576px)"
render={() => <WalletView />}
/>
<TransactionView />
</div>
</div>
)
}
}

View File

@ -0,0 +1,28 @@
import Home from './home.component'
import { compose } from 'recompose'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { unconfirmedTransactionsCountSelector } from '../../../selectors/confirm-transaction'
const mapStateToProps = state => {
const { metamask, appState } = state
const {
noActiveNotices,
lostAccounts,
seedWords,
} = metamask
const { forgottenPassword } = appState
return {
noActiveNotices,
lostAccounts,
forgottenPassword,
seedWords,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}
export default compose(
withRouter,
connect(mapStateToProps)
)(Home)

View File

@ -0,0 +1 @@
export { default } from './home.container'

View File

@ -1,56 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const AccountPanel = require('./account-panel')
PendingMsgDetails.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(PendingMsgDetails)
inherits(PendingMsgDetails, Component)
function PendingMsgDetails () {
Component.call(this)
}
PendingMsgDetails.prototype.render = function () {
var state = this.props
var msgData = state.txData
var msgParams = msgData.msgParams || {}
var address = msgParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return (
h('div', {
key: msgData.id,
style: {
margin: '10px 20px',
},
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
imageifyIdenticons: state.imageifyIdenticons,
}),
// message data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-column.flex-space-between', [
h('label.font-small.allcaps', this.context.t('message')),
h('span.font-small', msgParams.data),
]),
]),
])
)
}

View File

@ -1,73 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const PendingTxDetails = require('./pending-msg-details')
const connect = require('react-redux').connect
PendingMsg.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(PendingMsg)
inherits(PendingMsg, Component)
function PendingMsg () {
Component.call(this)
}
PendingMsg.prototype.render = function () {
var state = this.props
var msgData = state.txData
return (
h('div', {
key: msgData.id,
style: {
maxWidth: '350px',
},
}, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, this.context.t('signMessage')),
h('.error', {
style: {
margin: '10px',
},
}, [
this.context.t('signNotice'),
h('a', {
href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
style: { color: 'rgb(247, 134, 28)' },
onClick: (event) => {
event.preventDefault()
const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
global.platform.openWindow({ url })
},
}, this.context.t('readMore')),
]),
// message details
h(PendingTxDetails, state),
// sign + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelMessage,
}, this.context.t('cancel')),
h('button', {
onClick: state.signMessage,
}, this.context.t('sign')),
]),
])
)
}

View File

@ -35,12 +35,13 @@ function ShiftListItem () {
}
ShiftListItem.prototype.render = function () {
return h('div.tx-list-item.tx-list-clickable', {
return h('div.transaction-list-item.tx-list-clickable', {
style: {
paddingTop: '20px',
paddingBottom: '20px',
justifyContent: 'space-around',
alignItems: 'center',
flexDirection: 'row',
},
}, [
h('div', {

View File

@ -1,120 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenTracker = require('eth-token-tracker')
const connect = require('react-redux').connect
const selectors = require('../selectors')
const log = require('loglevel')
function mapStateToProps (state) {
return {
userAddress: selectors.getSelectedAddress(state),
}
}
module.exports = connect(mapStateToProps)(TokenBalance)
inherits(TokenBalance, Component)
function TokenBalance () {
this.state = {
string: '',
symbol: '',
isLoading: true,
error: null,
}
Component.call(this)
}
TokenBalance.prototype.render = function () {
const state = this.state
const { symbol, string, isLoading } = state
const { balanceOnly } = this.props
return isLoading
? h('span', '')
: h('span.token-balance', [
h('span.hide-text-overflow.token-balance__amount', string),
!balanceOnly && h('span.token-balance__symbol', symbol),
])
}
TokenBalance.prototype.componentDidMount = function () {
this.createFreshTokenTracker()
}
TokenBalance.prototype.createFreshTokenTracker = function () {
if (this.tracker) {
// Clean up old trackers when refreshing:
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
if (!global.ethereumProvider) return
const { userAddress, token } = this.props
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: [token],
pollingInterval: 8000,
})
// Set up listener instances for cleaning up
this.balanceUpdater = this.updateBalance.bind(this)
this.showError = error => {
this.setState({ error, isLoading: false })
}
this.tracker.on('update', this.balanceUpdater)
this.tracker.on('error', this.showError)
this.tracker.updateBalances()
.then(() => {
this.updateBalance(this.tracker.serialize())
})
.catch((reason) => {
log.error(`Problem updating balances`, reason)
this.setState({ isLoading: false })
})
}
TokenBalance.prototype.componentDidUpdate = function (nextProps) {
const {
userAddress: oldAddress,
token: { address: oldTokenAddress },
} = this.props
const {
userAddress: newAddress,
token: { address: newTokenAddress },
} = nextProps
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return
this.setState({ isLoading: true })
this.createFreshTokenTracker()
}
TokenBalance.prototype.updateBalance = function (tokens = []) {
if (!this.tracker.running) {
return
}
const [{ string, symbol }] = tokens
this.setState({
string,
symbol,
isLoading: false,
})
}
TokenBalance.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}

View File

@ -0,0 +1 @@
export { default } from './token-balance.container'

View File

@ -0,0 +1,23 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class TokenBalance extends PureComponent {
static propTypes = {
string: PropTypes.string,
symbol: PropTypes.string,
error: PropTypes.string,
className: PropTypes.string,
withSymbol: PropTypes.bool,
}
render () {
const { className, string, withSymbol, symbol } = this.props
return (
<div className={classnames('hide-text-overflow', className)}>
{ string + (withSymbol ? ` ${symbol}` : '') }
</div>
)
}
}

View File

@ -1,8 +1,8 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import withTokenTracker from '../../../../helpers/with-token-tracker'
import withTokenTracker from '../../higher-order-components/with-token-tracker'
import TokenBalance from './token-balance.component'
import selectors from '../../../../selectors'
import selectors from '../../selectors'
const mapStateToProps = state => {
return {

View File

@ -0,0 +1 @@
export { default } from './token-currency-display.component'

View File

@ -0,0 +1,54 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../currency-display/currency-display.component'
import { getTokenData } from '../../helpers/transactions.util'
import { calcTokenAmount } from '../../token-util'
export default class TokenCurrencyDisplay extends PureComponent {
static propTypes = {
transactionData: PropTypes.string,
token: PropTypes.object,
}
state = {
displayValue: '',
}
componentDidMount () {
this.setDisplayValue()
}
componentDidUpdate (prevProps) {
const { transactionData } = this.props
const { transactionData: prevTransactionData } = prevProps
if (transactionData !== prevTransactionData) {
this.setDisplayValue()
}
}
setDisplayValue () {
const { transactionData: data, token } = this.props
const { decimals = '', symbol = '' } = token
const tokenData = getTokenData(data)
let displayValue
if (tokenData.params && tokenData.params.length === 2) {
const tokenValue = tokenData.params[1].value
const tokenAmount = calcTokenAmount(tokenValue, decimals)
displayValue = `${tokenAmount} ${symbol}`
}
this.setState({ displayValue })
}
render () {
return (
<CurrencyDisplay
{...this.props}
displayValue={this.state.displayValue}
/>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './transaction-action.component'

View File

@ -0,0 +1,112 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import TransactionAction from '../transaction-action.component'
describe('TransactionAction Component', () => {
const tOrDefault = key => key
global.eth = {
getCode: sinon.stub().callsFake(address => {
console.log('CALLED')
const code = address === 'approveAddress' ? 'contract' : '0x'
return Promise.resolve(code)
}),
}
describe('Outgoing transaction', () => {
it('should render -- when methodData is still fetching', () => {
const methodData = { data: {}, done: false, error: null }
const transaction = {
id: 1,
status: 'confirmed',
submittedTime: 1534045442919,
time: 1534045440641,
txParams: {
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0x96',
to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(<TransactionAction
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
assert.equal(wrapper.find('.transaction-action').length, 1)
assert.equal(wrapper.text(), '--')
})
it('should render Sent Ether', () => {
const methodData = { data: {}, done: true, error: null }
const transaction = {
id: 1,
status: 'confirmed',
submittedTime: 1534045442919,
time: 1534045440641,
txParams: {
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0x96',
to: 'sentEtherAddress',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(<TransactionAction
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
assert.equal(wrapper.find('.transaction-action').length, 1)
wrapper.setState({ transactionAction: 'sentEther' })
assert.equal(wrapper.text(), 'sentEther')
})
it('should render Approved', () => {
const methodData = {
data: {
name: 'Approve',
params: [
{ type: 'address' },
{ type: 'uint256' },
],
},
done: true,
error: null,
}
const transaction = {
id: 1,
status: 'confirmed',
submittedTime: 1534045442919,
time: 1534045440641,
txParams: {
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0x96',
to: 'approveAddress',
value: '0x2386f26fc10000',
data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003',
},
}
const wrapper = shallow(<TransactionAction
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
assert.equal(wrapper.find('.transaction-action').length, 1)
wrapper.setState({ transactionAction: 'approve' })
assert.equal(wrapper.text(), 'approve')
})
})
})

View File

@ -0,0 +1,52 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { getTransactionActionKey } from '../../helpers/transactions.util'
export default class TransactionAction extends PureComponent {
static contextTypes = {
tOrDefault: PropTypes.func,
}
static propTypes = {
className: PropTypes.string,
transaction: PropTypes.object,
methodData: PropTypes.object,
}
state = {
transactionAction: '',
}
componentDidMount () {
this.getTransactionAction()
}
componentDidUpdate () {
this.getTransactionAction()
}
async getTransactionAction () {
const { transactionAction } = this.state
const { transaction, methodData } = this.props
const { data, done } = methodData
if (!done || transactionAction) {
return
}
const actionKey = await getTransactionActionKey(transaction, data)
const action = actionKey && this.context.tOrDefault(actionKey)
this.setState({ transactionAction: action })
}
render () {
const { className, methodData: { done } } = this.props
const { transactionAction } = this.state
return (
<div className={className}>
{ (done && transactionAction) || '--' }
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './transaction-list-item.container'

View File

@ -0,0 +1,117 @@
.transaction-list-item {
box-sizing: border-box;
min-height: 74px;
padding: 8px 20px;
border-bottom: 1px solid $geyser;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
@media screen and (max-width: $break-small) {
padding: 8px 20px 12px;
}
&:hover {
background: rgba($alto, .2);
}
&__grid {
width: 100%;
display: grid;
grid-template-columns: 45px 1fr 1fr 1fr;
grid-template-areas:
"identicon action status primary-amount"
"identicon nonce status secondary-amount";
@media screen and (max-width: $break-small) {
grid-template-columns: 45px 5fr 3fr;
grid-template-areas:
"nonce nonce nonce"
"identicon action primary-amount"
"identicon status secondary-amount";
}
}
&__identicon {
grid-area: identicon;
grid-row: 1 / span 2;
align-self: center;
@media screen and (max-width: $break-small) {
grid-row: 2 / span 2;
}
}
&__action {
text-transform: capitalize;
padding: 0 8px 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
grid-area: action;
align-self: end;
}
&__status {
grid-area: status;
grid-row: 1 / span 2;
align-self: center;
@media screen and (max-width: $break-small) {
grid-row: 3;
}
}
&__nonce {
font-size: .75rem;
color: #5e6064;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
grid-area: nonce;
align-self: start;
@media screen and (max-width: $break-small) {
padding-bottom: 4px;
}
}
&__amount {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&--primary {
text-align: end;
grid-area: primary-amount;
align-self: end;
@media screen and (max-width: $break-small) {
padding-bottom: 2px;
}
}
&--secondary {
text-align: end;
font-size: .75rem;
color: #5e6064;
grid-area: secondary-amount;
align-self: start;
}
}
&__retry {
background: #d1edff;
border-radius: 12px;
font-size: .75rem;
padding: 4px 12px;
cursor: pointer;
margin-top: 8px;
@media screen and (max-width: $break-small) {
font-size: .5rem;
}
}
}

View File

@ -0,0 +1,148 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Identicon from '../identicon'
import TransactionStatus from '../transaction-status'
import TransactionAction from '../transaction-action'
import CurrencyDisplay from '../currency-display'
import TokenCurrencyDisplay from '../token-currency-display'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
import { ETH } from '../../constants/common'
export default class TransactionListItem extends PureComponent {
static propTypes = {
history: PropTypes.object,
transaction: PropTypes.object,
value: PropTypes.string,
methodData: PropTypes.object,
showRetry: PropTypes.bool,
retryTransaction: PropTypes.func,
setSelectedToken: PropTypes.func,
nonceAndDate: PropTypes.string,
token: PropTypes.object,
}
handleClick = () => {
const { transaction, history } = this.props
const { id, status, hash, metamaskNetworkId } = transaction
if (status === UNAPPROVED_STATUS) {
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
} else if (hash) {
const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
global.platform.openWindow({ url: etherscanUrl })
}
}
handleRetryClick = event => {
event.stopPropagation()
const {
transaction: { txParams: { to } = {} },
methodData: { name } = {},
setSelectedToken,
} = this.props
if (name === TOKEN_METHOD_TRANSFER) {
setSelectedToken(to)
}
this.resubmit()
}
resubmit () {
const { transaction: { id }, retryTransaction, history } = this.props
retryTransaction(id)
.then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
}
renderPrimaryCurrency () {
const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
return token
? (
<TokenCurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--primary"
token={token}
transactionData={data}
prefix="-"
/>
) : (
<CurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--primary"
value={value}
prefix="-"
/>
)
}
renderSecondaryCurrency () {
const { token, value } = this.props
return token
? null
: (
<CurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--secondary"
prefix="-"
value={value}
numberOfDecimals={2}
currency={ETH}
/>
)
}
render () {
const {
transaction,
methodData,
showRetry,
nonceAndDate,
} = this.props
const { txParams = {} } = transaction
return (
<div
className="transaction-list-item"
onClick={this.handleClick}
>
<div className="transaction-list-item__grid">
<Identicon
className="transaction-list-item__identicon"
address={txParams.to}
diameter={34}
/>
<TransactionAction
transaction={transaction}
methodData={methodData}
className="transaction-list-item__action"
/>
<div
className="transaction-list-item__nonce"
title={nonceAndDate}
>
{ nonceAndDate }
</div>
<TransactionStatus
className="transaction-list-item__status"
statusKey={transaction.status}
/>
{ this.renderPrimaryCurrency() }
{ this.renderSecondaryCurrency() }
</div>
{
showRetry && methodData.done && (
<div
className="transaction-list-item__retry"
onClick={this.handleRetryClick}
>
<span>Taking too long? Increase the gas price on your transaction</span>
</div>
)
}
</div>
)
}
}

View File

@ -0,0 +1,32 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import withMethodData from '../../higher-order-components/with-method-data'
import TransactionListItem from './transaction-list-item.component'
import { setSelectedToken, retryTransaction } from '../../actions'
import { hexToDecimal } from '../../helpers/conversions.util'
import { formatDate } from '../../util'
const mapStateToProps = (state, ownProps) => {
const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
return {
value,
nonceAndDate,
}
}
const mapDispatchToProps = dispatch => {
return {
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
withMethodData,
)(TransactionListItem)

View File

@ -0,0 +1 @@
export { default } from './transaction-list.container'

View File

@ -0,0 +1,46 @@
.transaction-list {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: hidden;
&__completed-transactions {
display: flex;
flex-direction: column;
height: 100%;
}
&__header {
flex: 0 0 auto;
font-size: .875rem;
color: $dusty-gray;
border-bottom: 1px solid $geyser;
padding: 16px 0 8px 20px;
@media screen and (max-width: $break-small) {
padding: 8px 0 8px 16px;
}
}
&__transactions {
flex: 1;
overflow-y: auto;
}
&__pending-transactions {
margin-bottom: 16px;
}
&__empty {
flex: 1;
display: grid;
grid-template-rows: 35% 1fr;
}
&__empty-text {
grid-row-start: 2;
display: flex;
justify-content: center;
color: $silver;
}
}

View File

@ -0,0 +1,117 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import TransactionListItem from '../transaction-list-item'
import ShapeShiftTransactionListItem from '../shift-list-item'
import { TRANSACTION_TYPE_SHAPESHIFT } from '../../constants/transactions'
export default class TransactionList extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static defaultProps = {
pendingTransactions: [],
completedTransactions: [],
transactionToRetry: {},
}
static propTypes = {
pendingTransactions: PropTypes.array,
completedTransactions: PropTypes.array,
transactionToRetry: PropTypes.object,
selectedToken: PropTypes.object,
updateNetworkNonce: PropTypes.func,
}
componentDidMount () {
this.props.updateNetworkNonce()
}
componentDidUpdate (prevProps) {
const { pendingTransactions: prevPendingTransactions = [] } = prevProps
const { pendingTransactions = [], updateNetworkNonce } = this.props
if (pendingTransactions.length > prevPendingTransactions.length) {
updateNetworkNonce()
}
}
shouldShowRetry = transaction => {
const { transactionToRetry } = this.props
const { id, submittedTime } = transaction
return id === transactionToRetry.id && Date.now() - submittedTime > 30000
}
renderTransactions () {
const { t } = this.context
const { pendingTransactions = [], completedTransactions = [] } = this.props
return (
<div className="transaction-list__transactions">
{
pendingTransactions.length > 0 && (
<div className="transaction-list__pending-transactions">
<div className="transaction-list__header">
{ `${t('queue')} (${pendingTransactions.length})` }
</div>
{
pendingTransactions.map((transaction, index) => (
this.renderTransaction(transaction, index)
))
}
</div>
)
}
<div className="transaction-list__completed-transactions">
<div className="transaction-list__header">
{ t('history') }
</div>
{
completedTransactions.length > 0
? completedTransactions.map((transaction, index) => (
this.renderTransaction(transaction, index)
))
: this.renderEmpty()
}
</div>
</div>
)
}
renderTransaction (transaction, index) {
const { selectedToken } = this.props
return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
? (
<ShapeShiftTransactionListItem
{ ...transaction }
key={`shapeshift${index}`}
/>
) : (
<TransactionListItem
transaction={transaction}
key={transaction.id}
showRetry={this.shouldShowRetry(transaction)}
token={selectedToken}
/>
)
}
renderEmpty () {
return (
<div className="transaction-list__empty">
<div className="transaction-list__empty-text">
{ this.context.t('noTransactions') }
</div>
</div>
)
}
render () {
return (
<div className="transaction-list">
{ this.renderTransactions() }
</div>
)
}
}

View File

@ -0,0 +1,50 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TransactionList from './transaction-list.component'
import {
pendingTransactionsSelector,
submittedPendingTransactionsSelector,
completedTransactionsSelector,
} from '../../selectors/transactions'
import { getSelectedAddress } from '../../selectors'
import { selectedTokenSelector } from '../../selectors/tokens'
import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util'
import { updateNetworkNonce } from '../../actions'
const mapStateToProps = state => {
const pendingTransactions = pendingTransactionsSelector(state)
const submittedPendingTransactions = submittedPendingTransactionsSelector(state)
const networkNonce = state.appState.networkNonce
return {
completedTransactions: completedTransactionsSelector(state),
pendingTransactions,
transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce),
selectedToken: selectedTokenSelector(state),
selectedAddress: getSelectedAddress(state),
}
}
const mapDispatchToProps = dispatch => {
return {
updateNetworkNonce: address => dispatch(updateNetworkNonce(address)),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { selectedAddress, ...restStateProps } = stateProps
const { updateNetworkNonce, ...restDispatchProps } = dispatchProps
return {
...restStateProps,
...restDispatchProps,
...ownProps,
updateNetworkNonce: () => updateNetworkNonce(selectedAddress),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps, mergeProps)
)(TransactionList)

View File

@ -0,0 +1 @@
export { default } from './transaction-status.component'

View File

@ -0,0 +1,28 @@
.transaction-status {
height: 26px;
width: 81px;
border-radius: 4px;
background-color: #f0f0f0;
color: #5e6064;
font-size: .625rem;
text-transform: uppercase;
display: flex;
justify-content: center;
align-items: center;
@media screen and (max-width: $break-small) {
height: 16px;
width: 70px;
font-size: .5rem;
}
&--confirmed {
background-color: #eafad7;
color: #609a1c;
}
&--approved, &--submitted {
background-color: #FFF2DB;
color: #CA810A;
}
}

View File

@ -0,0 +1,51 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {
UNAPPROVED_STATUS,
REJECTED_STATUS,
APPROVED_STATUS,
SIGNED_STATUS,
SUBMITTED_STATUS,
CONFIRMED_STATUS,
FAILED_STATUS,
DROPPED_STATUS,
} from '../../constants/transactions'
const statusToClassNameHash = {
[UNAPPROVED_STATUS]: 'transaction-status--unapproved',
[REJECTED_STATUS]: 'transaction-status--rejected',
[APPROVED_STATUS]: 'transaction-status--approved',
[SIGNED_STATUS]: 'transaction-status--signed',
[SUBMITTED_STATUS]: 'transaction-status--submitted',
[CONFIRMED_STATUS]: 'transaction-status--confirmed',
[FAILED_STATUS]: 'transaction-status--failed',
[DROPPED_STATUS]: 'transaction-status--dropped',
}
const statusToTextHash = {
[APPROVED_STATUS]: 'pending',
[SUBMITTED_STATUS]: 'pending',
}
export default class TransactionStatus extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
statusKey: PropTypes.string,
className: PropTypes.string,
}
render () {
const { className, statusKey } = this.props
const statusText = this.context.t(statusToTextHash[statusKey] || statusKey)
return (
<div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
{ statusText }
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './transaction-view-balance.container'

View File

@ -0,0 +1,76 @@
.transaction-view-balance {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
height: 54px;
&__balance {
margin-left: 12px;
display: flex;
flex-direction: column;
@media screen and (max-width: $break-small) {
align-items: center;
margin: 16px 0;
}
}
&__token-balance {
margin-left: 12px;
font-size: 1.5rem;
@media screen and (max-width: $break-small) {
margin-bottom: 12px;
font-size: 1.75rem;
}
}
&__primary-balance {
font-size: 1.5rem;
@media screen and (max-width: $break-small) {
margin-bottom: 12px;
font-size: 1.75rem;
}
}
&__secondary-balance {
font-size: 1.15rem;
color: #a0a0a0;
}
&__balance-container {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
@media screen and (max-width: $break-small) {
flex-direction: column;
}
}
&__buttons {
display: flex;
flex-direction: row;
@media screen and (max-width: $break-small) {
margin-bottom: 16px;
}
}
&__button {
min-width: initial;
width: 100px;
&:not(:last-child) {
margin-right: 12px;
}
}
@media screen and (max-width: $break-small) {
flex-direction: column;
height: initial
}
}

View File

@ -0,0 +1,71 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import TokenBalance from '../../token-balance'
import CurrencyDisplay from '../../currency-display'
import { SEND_ROUTE } from '../../../routes'
import TransactionViewBalance from '../transaction-view-balance.component'
const propsMethodSpies = {
showDepositModal: sinon.spy(),
}
const historySpies = {
push: sinon.spy(),
}
const t = (str1, str2) => str2 ? str1 + str2 : str1
describe('TransactionViewBalance Component', () => {
afterEach(() => {
propsMethodSpies.showDepositModal.resetHistory()
historySpies.push.resetHistory()
})
it('should render ETH balance properly', () => {
const wrapper = shallow(<TransactionViewBalance
showDepositModal={propsMethodSpies.showDepositModal}
history={historySpies}
network="3"
ethBalance={123}
fiatBalance={456}
currentCurrency="usd"
/>, { context: { t } })
assert.equal(wrapper.find('.transaction-view-balance').length, 1)
assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
assert.equal(wrapper.find(CurrencyDisplay).length, 2)
const buttons = wrapper.find('.transaction-view-balance__buttons')
assert.equal(propsMethodSpies.showDepositModal.callCount, 0)
buttons.childAt(0).simulate('click')
assert.equal(propsMethodSpies.showDepositModal.callCount, 1)
assert.equal(historySpies.push.callCount, 0)
buttons.childAt(1).simulate('click')
assert.equal(historySpies.push.callCount, 1)
assert.equal(historySpies.push.getCall(0).args[0], SEND_ROUTE)
})
it('should render token balance properly', () => {
const token = {
address: '0x35865238f0bec9d5ce6abff0fdaebe7b853dfcc5',
decimals: '2',
symbol: 'ABC',
}
const wrapper = shallow(<TransactionViewBalance
showDepositModal={propsMethodSpies.showDepositModal}
history={historySpies}
network="3"
ethBalance={123}
fiatBalance={456}
currentCurrency="usd"
selectedToken={token}
/>, { context: { t } })
assert.equal(wrapper.find('.transaction-view-balance').length, 1)
assert.equal(wrapper.find('.transaction-view-balance__button').length, 1)
assert.equal(wrapper.find(TokenBalance).length, 1)
})
})

View File

@ -0,0 +1,94 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../button'
import Identicon from '../identicon'
import TokenBalance from '../token-balance'
import CurrencyDisplay from '../currency-display'
import { SEND_ROUTE } from '../../routes'
import { ETH } from '../../constants/common'
export default class TransactionViewBalance extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
showDepositModal: PropTypes.func,
selectedToken: PropTypes.object,
history: PropTypes.object,
network: PropTypes.string,
balance: PropTypes.string,
}
renderBalance () {
const { selectedToken, balance } = this.props
return selectedToken
? (
<TokenBalance
token={selectedToken}
withSymbol
className="transaction-view-balance__token-balance"
/>
) : (
<div className="transaction-view-balance__balance">
<CurrencyDisplay
className="transaction-view-balance__primary-balance"
value={balance}
currency={ETH}
numberOfDecimals={3}
/>
<CurrencyDisplay
className="transaction-view-balance__secondary-balance"
value={balance}
/>
</div>
)
}
renderButtons () {
const { t } = this.context
const { selectedToken, showDepositModal, history } = this.props
return (
<div className="transaction-view-balance__buttons">
{
!selectedToken && (
<Button
type="primary"
className="transaction-view-balance__button"
onClick={() => showDepositModal()}
>
{ t('deposit') }
</Button>
)
}
<Button
type="primary"
className="transaction-view-balance__button"
onClick={() => history.push(SEND_ROUTE)}
>
{ t('send') }
</Button>
</div>
)
}
render () {
const { network, selectedToken } = this.props
return (
<div className="transaction-view-balance">
<div className="transaction-view-balance__balance-container">
<Identicon
diameter={50}
address={selectedToken && selectedToken.address}
network={network}
/>
{ this.renderBalance() }
</div>
{ this.renderButtons() }
</div>
)
}
}

View File

@ -0,0 +1,30 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TransactionViewBalance from './transaction-view-balance.component'
import { getSelectedToken, getSelectedAddress } from '../../selectors'
import { showModal } from '../../actions'
const mapStateToProps = state => {
const selectedAddress = getSelectedAddress(state)
const { metamask: { network, accounts } } = state
const account = accounts[selectedAddress]
const { balance } = account
return {
selectedToken: getSelectedToken(state),
network,
balance,
}
}
const mapDispatchToProps = dispatch => {
return {
showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TransactionViewBalance)

View File

@ -0,0 +1 @@
export { default } from './transaction-view.component'

View File

@ -0,0 +1,27 @@
.transaction-view {
flex: 1 1 66.5%;
background: $white;
min-width: 0;
display: flex;
flex-direction: column;
&__balance-wrapper {
@media screen and (max-width: $break-small) {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex: 0 0 auto;
padding-top: 16px;
}
@media screen and (min-width: $break-large) {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin: 2.3em 2.37em .8em;
flex: 0 0 auto;
}
}
}

View File

@ -0,0 +1,27 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Media from 'react-media'
import MenuBar from '../menu-bar'
import TransactionViewBalance from '../transaction-view-balance'
import TransactionList from '../transaction-list'
export default class TransactionView extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
render () {
return (
<div className="transaction-view">
<Media
query="(max-width: 575px)"
render={() => <MenuBar />}
/>
<div className="transaction-view__balance-wrapper">
<TransactionViewBalance />
</div>
<TransactionList />
</div>
)
}
}

View File

@ -1,356 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const { compose } = require('recompose')
const { withRouter } = require('react-router-dom')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const inherits = require('util').inherits
const classnames = require('classnames')
const abi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
const Identicon = require('./identicon')
const contractMap = require('eth-contract-metadata')
const { checksumAddress } = require('../util')
const actions = require('../actions')
const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
const { calcTokenAmount } = require('../token-util')
const { getCurrentCurrency } = require('../selectors')
const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
TxListItem.contextTypes = {
t: PropTypes.func,
}
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TxListItem)
function mapStateToProps (state) {
return {
tokens: state.metamask.tokens,
currentCurrency: getCurrentCurrency(state),
contractExchangeRates: state.metamask.contractExchangeRates,
selectedAddressTxList: state.metamask.selectedAddressTxList,
networkNonce: state.appState.networkNonce,
}
}
function mapDispatchToProps (dispatch) {
return {
setSelectedToken: tokenAddress => dispatch(actions.setSelectedToken(tokenAddress)),
retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)),
}
}
inherits(TxListItem, Component)
function TxListItem () {
Component.call(this)
this.state = {
total: null,
fiatTotal: null,
isTokenTx: null,
}
this.unmounted = false
}
TxListItem.prototype.componentDidMount = async function () {
const { txParams = {} } = this.props
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { name: txDataName } = decodedData || {}
const isTokenTx = txDataName === 'transfer'
const { total, fiatTotal } = isTokenTx
? await this.getSendTokenTotal()
: this.getSendEtherTotal()
if (this.unmounted) {
return
}
this.setState({ total, fiatTotal, isTokenTx })
}
TxListItem.prototype.componentWillUnmount = function () {
this.unmounted = true
}
TxListItem.prototype.getAddressText = function () {
const {
address,
txParams = {},
isMsg,
} = this.props
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { name: txDataName, params = [] } = decodedData || {}
const { value } = params[0] || {}
const checksummedAddress = checksumAddress(address)
const checksummedValue = checksumAddress(value)
let addressText
if (txDataName === 'transfer' || address) {
const addressToRender = txDataName === 'transfer' ? checksummedValue : checksummedAddress
addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}`
} else if (isMsg) {
addressText = this.context.t('sigRequest')
} else {
addressText = this.context.t('contractDeployment')
}
return addressText
}
TxListItem.prototype.getSendEtherTotal = function () {
const {
transactionAmount,
conversionRate,
address,
currentCurrency,
} = this.props
if (!address) {
return {}
}
const totalInFiat = conversionUtil(transactionAmount, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: currentCurrency,
fromDenomination: 'WEI',
numberOfDecimals: 2,
conversionRate,
})
const totalInETH = conversionUtil(transactionAmount, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: 'ETH',
fromDenomination: 'WEI',
conversionRate,
numberOfDecimals: 6,
})
return {
total: `${totalInETH} ETH`,
fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`,
}
}
TxListItem.prototype.getTokenInfo = async function () {
const { txParams = {}, tokenInfoGetter, tokens } = this.props
const toAddress = txParams.to
let decimals
let symbol
({ decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {})
if (!decimals && !symbol) {
({ decimals, symbol } = contractMap[toAddress] || {})
}
if (!decimals && !symbol) {
({ decimals, symbol } = await tokenInfoGetter(toAddress))
}
return { decimals, symbol, address: toAddress }
}
TxListItem.prototype.getSendTokenTotal = async function () {
const {
txParams = {},
conversionRate,
contractExchangeRates,
currentCurrency,
} = this.props
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { params = [] } = decodedData || {}
const { value } = params[1] || {}
const { decimals, symbol, address } = await this.getTokenInfo()
const total = calcTokenAmount(value, decimals)
let tokenToFiatRate
let totalInFiat
if (contractExchangeRates[address]) {
tokenToFiatRate = multiplyCurrencies(
contractExchangeRates[address],
conversionRate
)
totalInFiat = conversionUtil(total, {
fromNumericBase: 'dec',
toNumericBase: 'dec',
fromCurrency: symbol,
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate: tokenToFiatRate,
})
}
const showFiat = Boolean(totalInFiat) && currentCurrency.toUpperCase() !== symbol
return {
total: `${total} ${symbol}`,
fiatTotal: showFiat && `${totalInFiat} ${currentCurrency.toUpperCase()}`,
}
}
TxListItem.prototype.showRetryButton = function () {
const {
transactionSubmittedTime,
selectedAddressTxList,
transactionId,
txParams,
networkNonce,
} = this.props
if (!txParams) {
return false
}
let currentTxSharesEarliestNonce = false
const currentNonce = txParams.nonce
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transactionId
if (currentSubmittedTxs.length > 0) {
currentTxSharesEarliestNonce = currentNonce === networkNonce
}
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
}
TxListItem.prototype.setSelectedToken = function (tokenAddress) {
this.props.setSelectedToken(tokenAddress)
}
TxListItem.prototype.resubmit = function () {
const { transactionId } = this.props
this.props.retryTransaction(transactionId)
.then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
}
TxListItem.prototype.render = function () {
const {
transactionStatus,
onClick,
transactionId,
dateString,
address,
className,
txParams,
} = this.props
const { total, fiatTotal, isTokenTx } = this.state
return h(`div${className || ''}`, {
key: transactionId,
onClick: () => onClick && onClick(transactionId),
}, [
h(`div.flex-column.tx-list-item-wrapper`, {}, [
h('div.tx-list-date-wrapper', {
style: {},
}, [
h('span.tx-list-date', {}, [
dateString,
]),
]),
h('div.flex-row.tx-list-content-wrapper', {
style: {},
}, [
h('div.tx-list-identicon-wrapper', {
style: {},
}, [
h(Identicon, {
address,
diameter: 28,
}),
]),
h('div.tx-list-account-and-status-wrapper', {}, [
h('div.tx-list-account-wrapper', {
style: {},
}, [
h('span.tx-list-account', {}, [
this.getAddressText(address),
]),
]),
h('div.tx-list-status-wrapper', {
style: {},
}, [
h('span', {
className: classnames('tx-list-status', {
'tx-list-status--rejected': transactionStatus === 'rejected',
'tx-list-status--failed': transactionStatus === 'failed',
'tx-list-status--dropped': transactionStatus === 'dropped',
}),
},
this.txStatusIndicator(),
),
]),
]),
h('div.flex-column.tx-list-details-wrapper', {
style: {},
}, [
h('span.tx-list-value', total),
fiatTotal && h('span.tx-list-fiat-value', fiatTotal),
]),
]),
this.showRetryButton() && h('.tx-list-item-retry-container', {
onClick: (event) => {
event.stopPropagation()
if (isTokenTx) {
this.setSelectedToken(txParams.to)
}
this.resubmit()
},
}, [
h('span', 'Taking too long? Increase the gas price on your transaction'),
]),
]), // holding on icon from design
])
}
TxListItem.prototype.txStatusIndicator = function () {
const { transactionStatus } = this.props
let name
if (transactionStatus === 'unapproved') {
name = this.context.t('unapproved')
} else if (transactionStatus === 'rejected') {
name = this.context.t('rejected')
} else if (transactionStatus === 'approved') {
name = this.context.t('approved')
} else if (transactionStatus === 'signed') {
name = this.context.t('signed')
} else if (transactionStatus === 'submitted') {
name = this.context.t('submitted')
} else if (transactionStatus === 'confirmed') {
name = this.context.t('confirmed')
} else if (transactionStatus === 'failed') {
name = this.context.t('failed')
} else if (transactionStatus === 'dropped') {
name = this.context.t('dropped')
}
return name
}

View File

@ -1,171 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
const selectors = require('../selectors')
const TxListItem = require('./tx-list-item')
const ShiftListItem = require('./shift-list-item')
const { formatDate } = require('../util')
const { showConfTxPage, updateNetworkNonce } = require('../actions')
const classnames = require('classnames')
const { tokenInfoGetter } = require('../token-util')
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TxList)
TxList.contextTypes = {
t: PropTypes.func,
}
function mapStateToProps (state) {
return {
txsToRender: selectors.transactionsSelector(state),
conversionRate: selectors.conversionRateSelector(state),
selectedAddress: selectors.getSelectedAddress(state),
}
}
function mapDispatchToProps (dispatch) {
return {
showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
}
}
inherits(TxList, Component)
function TxList () {
Component.call(this)
}
TxList.prototype.componentWillMount = function () {
this.tokenInfoGetter = tokenInfoGetter()
this.props.updateNetworkNonce(this.props.selectedAddress)
}
TxList.prototype.componentDidUpdate = function (prevProps) {
const oldTxsToRender = prevProps.txsToRender
const {
txsToRender: newTxsToRender,
selectedAddress,
updateNetworkNonce,
} = this.props
if (newTxsToRender.length > oldTxsToRender.length) {
updateNetworkNonce(selectedAddress)
}
}
TxList.prototype.render = function () {
return h('div.flex-column', [
h('div.flex-row.tx-list-header-wrapper', [
h('div.flex-row.tx-list-header', [
h('div', this.context.t('transactions')),
]),
]),
h('div.flex-column.tx-list-container', {}, [
this.renderTransaction(),
]),
])
}
TxList.prototype.renderTransaction = function () {
const { txsToRender, conversionRate } = this.props
return txsToRender.length
? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate, i))
: [h(
'div.tx-list-item.tx-list-item--empty',
{ key: 'tx-list-none' },
[ this.context.t('noTransactions') ],
)]
}
// TODO: Consider moving TxListItem into a separate component
TxList.prototype.renderTransactionListItem = function (transaction, conversionRate, index) {
// console.log({transaction})
// refer to transaction-list.js:line 58
if (transaction.key === 'shapeshift') {
return h(ShiftListItem, { ...transaction, key: `shapeshift${index}` })
}
const props = {
dateString: formatDate(transaction.time),
address: transaction.txParams && transaction.txParams.to,
transactionStatus: transaction.status,
transactionAmount: transaction.txParams && transaction.txParams.value,
transactionId: transaction.id,
transactionHash: transaction.hash,
transactionNetworkId: transaction.metamaskNetworkId,
transactionSubmittedTime: transaction.submittedTime,
}
const {
address,
transactionStatus,
transactionAmount,
dateString,
transactionId,
transactionHash,
transactionNetworkId,
transactionSubmittedTime,
} = props
const { history } = this.props
const opts = {
key: transactionId || transactionHash,
txParams: transaction.txParams,
isMsg: Boolean(transaction.msgParams),
transactionStatus,
transactionId,
dateString,
address,
transactionAmount,
transactionHash,
conversionRate,
tokenInfoGetter: this.tokenInfoGetter,
transactionSubmittedTime,
}
const isUnapproved = transactionStatus === 'unapproved'
if (isUnapproved) {
opts.onClick = () => {
this.props.showConfTxPage({ id: transactionId })
history.push(CONFIRM_TRANSACTION_ROUTE)
}
opts.transactionStatus = this.context.t('notStarted')
} else if (transactionHash) {
opts.onClick = () => this.view(transactionHash, transactionNetworkId)
}
opts.className = classnames('.tx-list-item', {
'.tx-list-pending-item-container': isUnapproved,
'.tx-list-clickable': Boolean(transactionHash) || isUnapproved,
})
return h(TxListItem, opts)
}
TxList.prototype.view = function (txHash, network) {
const url = etherscanLinkFor(txHash, network)
if (url) {
navigateTo(url)
}
}
function navigateTo (url) {
global.platform.openWindow({ url })
}
function etherscanLinkFor (txHash, network) {
const prefix = prefixForNetwork(network)
return `https://${prefix}etherscan.io/tx/${txHash}`
}

View File

@ -1,156 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const actions = require('../actions')
const selectors = require('../selectors')
const { SEND_ROUTE } = require('../routes')
const { checksumAddress: toChecksumAddress } = require('../util')
const BalanceComponent = require('./balance-component')
const Tooltip = require('./tooltip')
const TxList = require('./tx-list')
const SelectedAccount = require('./selected-account')
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TxView)
TxView.contextTypes = {
t: PropTypes.func,
}
function mapStateToProps (state) {
const sidebarOpen = state.appState.sidebarOpen
const isMascara = state.appState.isMascara
const identities = state.metamask.identities
const accounts = state.metamask.accounts
const network = state.metamask.network
const selectedTokenAddress = state.metamask.selectedTokenAddress
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const checksumAddress = toChecksumAddress(selectedAddress)
const identity = identities[selectedAddress]
return {
sidebarOpen,
selectedAddress,
checksumAddress,
selectedTokenAddress,
selectedToken: selectors.getSelectedToken(state),
identity,
network,
isMascara,
}
}
function mapDispatchToProps (dispatch) {
return {
showSidebar: () => { dispatch(actions.showSidebar()) },
hideSidebar: () => { dispatch(actions.hideSidebar()) },
showModal: (payload) => { dispatch(actions.showModal(payload)) },
showSendPage: () => { dispatch(actions.showSendPage()) },
showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) },
}
}
inherits(TxView, Component)
function TxView () {
Component.call(this)
}
TxView.prototype.renderHeroBalance = function () {
const { selectedToken } = this.props
return h('div.hero-balance', {}, [
h(BalanceComponent, { token: selectedToken }),
this.renderButtons(),
])
}
TxView.prototype.renderButtons = function () {
const {selectedToken, showModal, history } = this.props
return !selectedToken
? (
h('div.flex-row.flex-center.hero-balance-buttons', [
h('button.btn-primary.hero-balance-button', {
onClick: () => showModal({
name: 'DEPOSIT_ETHER',
}),
}, this.context.t('deposit')),
h('button.btn-primary.hero-balance-button', {
style: {
marginLeft: '0.8em',
},
onClick: () => history.push(SEND_ROUTE),
}, this.context.t('send')),
])
)
: (
h('div.flex-row.flex-center.hero-balance-buttons', [
h('button.btn-primary.hero-balance-button', {
onClick: () => history.push(SEND_ROUTE),
}, this.context.t('send')),
])
)
}
TxView.prototype.render = function () {
const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props
const { t } = this.context
return h('div.tx-view.flex-column', {
style: {},
}, [
h('div.flex-row.phone-visible', {
style: {
justifyContent: 'center',
alignItems: 'center',
flex: '0 0 auto',
marginBottom: '16px',
padding: '5px',
borderBottom: '1px solid #e5e5e5',
},
}, [
h(Tooltip, {
title: t('menu'),
position: 'bottom',
}, [
h('div.fa.fa-bars', {
style: {
fontSize: '1.3em',
cursor: 'pointer',
padding: '10px',
},
onClick: () => sidebarOpen ? hideSidebar() : showSidebar(),
}),
]),
h(SelectedAccount),
!isMascara && h(Tooltip, {
title: t('openInTab'),
position: 'bottom',
}, [
h('div.open-in-browser', {
onClick: () => global.platform.openExtensionInBrowser(),
}, [h('img', { src: 'images/popout.svg' })]),
]),
]),
this.renderHeroBalance(),
h(TxList),
])
}

View File

@ -26,6 +26,10 @@ WalletView.contextTypes = {
t: PropTypes.func,
}
WalletView.defaultProps = {
responsiveDisplayClassname: '',
}
function mapStateToProps (state) {
return {
@ -131,8 +135,9 @@ WalletView.prototype.render = function () {
}
}
return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
return h('div.wallet-view.flex-column', {
style: {},
className: responsiveDisplayClassname,
}, [
// TODO: Separate component: wallet account details

View File

@ -0,0 +1 @@
export const ETH = 'ETH'

View File

@ -0,0 +1,22 @@
export const UNAPPROVED_STATUS = 'unapproved'
export const REJECTED_STATUS = 'rejected'
export const APPROVED_STATUS = 'approved'
export const SIGNED_STATUS = 'signed'
export const SUBMITTED_STATUS = 'submitted'
export const CONFIRMED_STATUS = 'confirmed'
export const FAILED_STATUS = 'failed'
export const DROPPED_STATUS = 'dropped'
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
export const SEND_ETHER_ACTION_KEY = 'sentEther'
export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
export const APPROVE_ACTION_KEY = 'approve'
export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
export const UNKNOWN_FUNCTION_KEY = 'unknownFunction'
export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'

View File

@ -1,130 +0,0 @@
.hero-balance {
@media screen and (max-width: $break-small) {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex: 0 0 auto;
padding-top: 16px;
}
@media screen and (min-width: $break-large) {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin: 2.3em 2.37em .8em;
flex: 0 0 auto;
}
.balance-container {
display: flex;
margin: 0;
justify-content: flex-start;
align-items: center;
@media screen and (max-width: $break-small) {
flex-direction: column;
flex: 0 0 auto;
max-width: 100%;
}
@media screen and (min-width: $break-large) {
flex-direction: row;
flex-grow: 3;
min-width: 0;
}
}
.balance-display {
.token-amount {
color: $black;
max-width: 100%;
.token-balance {
display: flex;
}
}
@media screen and (max-width: $break-small) {
max-width: 100%;
text-align: center;
.token-amount {
font-size: 1.75rem;
margin-top: 1rem;
.token-balance {
flex-direction: column;
}
}
.fiat-amount {
font-size: 115%;
margin-top: 8.5%;
color: #a0a0a0;
}
}
@media screen and (min-width: $break-large) {
margin: 0 .8em;
justify-content: flex-start;
align-items: flex-start;
min-width: 0;
.token-amount {
font-size: 1.5rem;
}
.fiat-amount {
margin-top: .25%;
font-size: 105%;
}
}
@media #{$sub-mid-size-breakpoint-range} {
margin-left: .4em;
margin-right: .4em;
justify-content: flex-start;
align-items: flex-start;
.token-amount {
font-size: 1rem;
}
.fiat-amount {
margin-top: .25%;
font-size: 1rem;
}
}
}
.hero-balance-buttons {
@media screen and (max-width: $break-small) {
width: 100%;
// height: 100px; // needed a round number to set the heights of the buttons inside
flex: 0 0 auto;
padding: 16px 0;
}
@media screen and (min-width: $break-large) {
flex-grow: 2;
justify-content: flex-end;
}
}
}
.hero-balance-button {
min-width: initial;
width: 6rem;
@media #{$sub-mid-size-breakpoint-range} {
padding: .4rem;
width: 4rem;
display: flex;
flex: 1;
justify-content: center;
}
}

View File

@ -19,8 +19,6 @@
@import './loading-overlay.scss';
// Balances
@import './hero-balance.scss';
@import './wallet-balance.scss';
// Tx List and Sections

View File

@ -6,7 +6,6 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma
*/
// Component Colors
$tx-view-bg: $white;
$wallet-view-bg: $alabaster;
// Main container
@ -30,32 +29,6 @@ $wallet-view-bg: $alabaster;
min-width: 0;
}
// tx view
.tx-view {
flex: 1 1 66.5%;
background: $tx-view-bg;
min-width: 0;
// No title on mobile
@media screen and (max-width: 575px) {
.identicon-wrapper {
display: none;
}
.account-name {
display: none;
}
}
}
.open-in-browser {
cursor: pointer;
display: flex;
justify-content: center;
padding: 10px;
}
// wallet view and sidebar
.wallet-view {

View File

@ -243,7 +243,7 @@
}
.tx-list-item {
border-top: 1px solid rgb(231, 231, 231);
border-bottom: 1px solid $geyser;
flex: 0 0 auto;
display: flex;
flex-flow: row nowrap;

View File

@ -5,18 +5,16 @@ import {
} from '../selectors/confirm-transaction'
import {
getTokenData,
getMethodData,
getTransactionAmount,
getValueFromWeiHex,
getTransactionFee,
getHexGasTotal,
addFiat,
addEth,
increaseLastGasPrice,
hexGreaterThan,
isSmartContractAddress,
} from '../helpers/confirm-transaction/util'
import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util'
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
@ -301,10 +299,10 @@ export function updateTxDataAndCalculate (txData) {
const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
const fiatTransactionAmount = getTransactionAmount({
const fiatTransactionAmount = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
const ethTransactionAmount = getTransactionAmount({
const ethTransactionAmount = getValueFromWeiHex({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
})

View File

@ -1,15 +1,8 @@
import currencyFormatter from 'currency-formatter'
import currencies from 'currency-formatter/currencies'
import abi from 'human-standard-token-abi'
import abiDecoder from 'abi-decoder'
import ethUtil from 'ethereumjs-util'
import BigNumber from 'bignumber.js'
abiDecoder.addABI(abi)
import MethodRegistry from 'eth-method-registry'
const registry = new MethodRegistry({ provider: global.ethereumProvider })
import {
conversionUtil,
addCurrencies,
@ -19,22 +12,6 @@ import {
import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
export function getTokenData (data = {}) {
return abiDecoder.decodeMethod(data)
}
export async function getMethodData (data = {}) {
const prefixedData = ethUtil.addHexPrefix(data)
const fourBytePrefix = prefixedData.slice(0, 10)
const sig = await registry.lookup(fourBytePrefix)
const parsedResult = registry.parse(sig)
return {
name: parsedResult.name,
params: parsedResult.args,
}
}
export function increaseLastGasPrice (lastGasPrice) {
return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
@ -76,7 +53,7 @@ export function addFiat (...args) {
})
}
export function getTransactionAmount ({
export function getValueFromWeiHex ({
value,
toCurrency,
conversionRate,
@ -146,8 +123,3 @@ export function roundExponential (value) {
// In JS, numbers with exponentials greater than 20 get displayed as an exponential.
return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value
}
export async function isSmartContractAddress (address) {
const code = await global.eth.getCode(address)
return code && code !== '0x'
}

View File

@ -92,9 +92,9 @@ describe('Confirm Transaction utils', () => {
})
})
describe('getTransactionAmount', () => {
describe('getValueFromWeiHex', () => {
it('should get the transaction amount in ETH', () => {
const ethTransactionAmount = utils.getTransactionAmount({
const ethTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
})
@ -102,7 +102,7 @@ describe('Confirm Transaction utils', () => {
})
it('should get the transaction amount in fiat', () => {
const fiatTransactionAmount = utils.getTransactionAmount({
const fiatTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
})

View File

@ -0,0 +1,37 @@
import { conversionUtil } from '../conversion-util'
export function hexToDecimal (hexValue) {
return conversionUtil(hexValue, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
}
export function getEthFromWeiHex ({
value,
conversionRate,
}) {
return getValueFromWeiHex({
value,
conversionRate,
toCurrency: 'ETH',
numberOfDecimals: 6,
})
}
export function getValueFromWeiHex ({
value,
toCurrency,
conversionRate,
numberOfDecimals,
}) {
return conversionUtil(value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency,
numberOfDecimals,
fromDenomination: 'WEI',
conversionRate,
})
}

View File

@ -0,0 +1,105 @@
import ethUtil from 'ethereumjs-util'
import MethodRegistry from 'eth-method-registry'
import abi from 'human-standard-token-abi'
import abiDecoder from 'abi-decoder'
import {
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM,
SEND_ETHER_ACTION_KEY,
DEPLOY_CONTRACT_ACTION_KEY,
APPROVE_ACTION_KEY,
SEND_TOKEN_ACTION_KEY,
TRANSFER_FROM_ACTION_KEY,
SIGNATURE_REQUEST_KEY,
UNKNOWN_FUNCTION_KEY,
} from '../constants/transactions'
abiDecoder.addABI(abi)
export function getTokenData (data = {}) {
return abiDecoder.decodeMethod(data)
}
const registry = new MethodRegistry({ provider: global.ethereumProvider })
export async function getMethodData (data = {}) {
const prefixedData = ethUtil.addHexPrefix(data)
const fourBytePrefix = prefixedData.slice(0, 10)
const sig = await registry.lookup(fourBytePrefix)
const parsedResult = registry.parse(sig)
return {
name: parsedResult.name,
params: parsedResult.args,
}
}
export function isConfirmDeployContract (txData = {}) {
const { txParams = {} } = txData
return !txParams.to
}
export async function getTransactionActionKey (transaction, methodData) {
const { txParams: { data, to } = {}, msgParams } = transaction
if (msgParams) {
return SIGNATURE_REQUEST_KEY
}
if (isConfirmDeployContract(transaction)) {
return DEPLOY_CONTRACT_ACTION_KEY
}
if (data) {
const toSmartContract = await isSmartContractAddress(to)
if (!toSmartContract) {
return SEND_ETHER_ACTION_KEY
}
const { name } = methodData
const methodName = name && name.toLowerCase()
if (!methodName) {
return UNKNOWN_FUNCTION_KEY
}
switch (methodName) {
case TOKEN_METHOD_TRANSFER:
return SEND_TOKEN_ACTION_KEY
case TOKEN_METHOD_APPROVE:
return APPROVE_ACTION_KEY
case TOKEN_METHOD_TRANSFER_FROM:
return TRANSFER_FROM_ACTION_KEY
default:
return name
}
} else {
return SEND_ETHER_ACTION_KEY
}
}
export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') {
if (!transactions.length) {
return {}
}
return transactions.reduce((acc, current) => {
const { submittedTime, txParams: { nonce: currentNonce } = {} } = current
if (currentNonce === nonce) {
return acc.submittedTime
? submittedTime > acc.submittedTime ? current : acc
: current
} else {
return acc
}
}, {})
}
export async function isSmartContractAddress (address) {
const code = await global.eth.getCode(address)
return code && code !== '0x'
}

View File

@ -0,0 +1 @@
export { default } from './with-method-data.component'

View File

@ -0,0 +1,52 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { getMethodData } from '../../helpers/transactions.util'
export default function withMethodData (WrappedComponent) {
return class MethodDataWrappedComponent extends PureComponent {
static propTypes = {
transaction: PropTypes.object,
}
static defaultProps = {
transaction: {},
}
state = {
methodData: {},
done: false,
error: null,
}
componentDidMount () {
this.fetchMethodData()
}
async fetchMethodData () {
const { transaction } = this.props
const { txParams: { data = '' } = {} } = transaction
if (data) {
try {
const methodData = await getMethodData(data)
this.setState({ methodData, done: true })
} catch (error) {
this.setState({ done: true, error })
}
} else {
this.setState({ done: true })
}
}
render () {
const { methodData, done, error } = this.state
return (
<WrappedComponent
{ ...this.props }
methodData={{ data: methodData, done, error }}
/>
)
}
}
}

View File

@ -0,0 +1 @@
export { default } from './with-token-tracker.component'

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TokenTracker from 'eth-token-tracker'
const withTokenTracker = WrappedComponent => {
export default function withTokenTracker (WrappedComponent) {
return class TokenTrackerWrappedComponent extends Component {
static propTypes = {
userAddress: PropTypes.string.isRequired,
@ -104,5 +104,3 @@ const withTokenTracker = WrappedComponent => {
}
}
}
module.exports = withTokenTracker

View File

@ -6,6 +6,11 @@ const { compose } = require('recompose')
const t = require('../i18n-helper').getMessage
class I18nProvider extends Component {
tOrDefault = (key, defaultValue, ...args) => {
const { localeMessages: { current, en } = {} } = this.props
return t(current, key, ...args) || t(en, key, ...args) || defaultValue
}
getChildContext () {
const { localeMessages } = this.props
const { current, en } = localeMessages
@ -13,6 +18,10 @@ class I18nProvider extends Component {
t (key, ...args) {
return t(current, key, ...args) || t(en, key, ...args) || `[${key}]`
},
tOrDefault: this.tOrDefault,
tOrKey (key, ...args) {
return this.tOrDefault(key, key, ...args)
},
}
}
@ -28,6 +37,8 @@ I18nProvider.propTypes = {
I18nProvider.childContextTypes = {
t: PropTypes.func,
tOrDefault: PropTypes.func,
tOrKey: PropTypes.func,
}
const mapStateToProps = state => {

View File

@ -1,49 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountAndTransactionDetails = require('./account-and-transaction-details')
const Settings = require('./components/pages/settings')
const log = require('loglevel')
import UnlockScreen from './components/pages/unlock-page'
module.exports = MainContainer
inherits(MainContainer, Component)
function MainContainer () {
Component.call(this)
}
MainContainer.prototype.render = function () {
// 3. summarize:
// switch statement goes inside MainContainer,
// or a method in renderPrimary
// - pass resulting h() to MainContainer
// - error checking in separate func
// - router in separate func
const contents = {
component: AccountAndTransactionDetails,
key: 'account-detail',
style: {},
}
if (this.props.isUnlocked === false) {
switch (this.props.currentViewName) {
case 'config':
log.debug('rendering config screen from unlock screen.')
return h(Settings, {key: 'config'})
default:
log.debug('rendering locked screen')
return h('.unlock-screen-container', {}, h(UnlockScreen, { key: 'locked' }))
}
}
return h('div.main-container', {
style: contents.style,
}, [
h(contents.component, {
key: contents.key,
}, []),
])
}

View File

@ -1,29 +0,0 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
module.exports = connect(mapStateToProps)(NewKeychain)
function mapStateToProps (state) {
return {}
}
inherits(NewKeychain, Component)
function NewKeychain () {
Component.call(this)
}
NewKeychain.prototype.render = function () {
// const props = this.props
return (
h('div', {
style: {
background: 'blue',
},
}, [
h('h1', `Here's a list!!!!`),
])
)
}

View File

@ -1,6 +1,9 @@
const valuesFor = require('./util').valuesFor
const abi = require('human-standard-token-abi')
import {
transactionsSelector,
} from './selectors/transactions'
const {
multiplyCurrencies,
} = require('./conversion-util')
@ -101,22 +104,6 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress)
}
function transactionsSelector (state) {
const { network, selectedTokenAddress } = state.metamask
const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
const transactions = state.metamask.selectedAddressTxList || []
const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
// console.log({txsToRender, selectedTokenAddress})
return selectedTokenAddress
? txsToRender
.filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
.sort((a, b) => b.time - a.time)
: txsToRender
.sort((a, b) => b.time - a.time)
}
function getGasIsLoading (state) {
return state.appState.gasIsLoading
}

View File

@ -0,0 +1,11 @@
import { createSelector } from 'reselect'
export const selectedTokenAddressSelector = state => state.metamask.selectedTokenAddress
export const tokenSelector = state => state.metamask.tokens
export const selectedTokenSelector = createSelector(
tokenSelector,
selectedTokenAddressSelector,
(tokens = [], selectedTokenAddress = '') => {
return tokens.find(({ address }) => address === selectedTokenAddress)
}
)

View File

@ -0,0 +1,58 @@
import { createSelector } from 'reselect'
import { valuesFor } from '../util'
import {
UNAPPROVED_STATUS,
APPROVED_STATUS,
SUBMITTED_STATUS,
} from '../constants/transactions'
import { selectedTokenAddressSelector } from './tokens'
export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList
export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList
const pendingStatusHash = {
[UNAPPROVED_STATUS]: true,
[APPROVED_STATUS]: true,
[SUBMITTED_STATUS]: true,
}
export const transactionsSelector = createSelector(
selectedTokenAddressSelector,
unapprovedMsgsSelector,
shapeShiftTxListSelector,
selectedAddressTxListSelector,
(selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => {
const unapprovedMsgsList = valuesFor(unapprovedMsgs)
const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList)
return selectedTokenAddress
? txsToRender
.filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
.sort((a, b) => b.time - a.time)
: txsToRender
.sort((a, b) => b.time - a.time)
}
)
export const pendingTransactionsSelector = createSelector(
transactionsSelector,
(transactions = []) => (
transactions.filter(transaction => transaction.status in pendingStatusHash)
)
)
export const submittedPendingTransactionsSelector = createSelector(
transactionsSelector,
(transactions = []) => (
transactions.filter(transaction => transaction.status === SUBMITTED_STATUS)
)
)
export const completedTransactionsSelector = createSelector(
transactionsSelector,
(transactions = []) => (
transactions.filter(transaction => !(transaction.status in pendingStatusHash))
)
)

View File

@ -9,7 +9,7 @@ const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
// formatData :: ( date: <Unix Timestamp> ) -> String
function formatDate (date) {
return vreme.format(new Date(date), 'March 16 2014 14:30')
return vreme.format(new Date(date), '3/16/2014 at 14:30')
}
var valueTable = {