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

Resolve merge conflict.

This commit is contained in:
Kevin Serrano 2016-07-07 16:33:06 -07:00
commit 86b7cc6637
21 changed files with 597 additions and 76 deletions

View File

@ -6,6 +6,7 @@
- Fix formatting of account details. - Fix formatting of account details.
- Use web3 minified dist for faster inject times - Use web3 minified dist for faster inject times
- Fix issue where dropdowns were not in front of icons. - Fix issue where dropdowns were not in front of icons.
- Update transaction approval styles.
- Align failed and successful transaction history text. - Align failed and successful transaction history text.
- Fix issue where large domain names and large transaction values would misalign the transaction history. - Fix issue where large domain names and large transaction values would misalign the transaction history.

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="4.2333331mm"
height="12.800793mm"
viewBox="0 0 14.999999 45.357139"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="forward-carrat.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="17.87049"
inkscape:cy="17.678567"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
showguides="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1276"
inkscape:window-height="755"
inkscape:window-x="4"
inkscape:window-y="1"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4136"
originx="-180"
originy="-602.14286" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-180,-404.8622)">
<path
style="fill:#f7861c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 180,404.8622 0,7.5 10,15 -10,15 0,7.85714 15,-22.85714 z"
id="path4138"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -18,7 +18,7 @@ html, body, #app-content, .super-dev-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
background: #cccccc; background: white;
} }
.mock-app-root { .mock-app-root {
background: #F7F7F7; background: #F7F7F7;

View File

@ -0,0 +1,84 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"name": "Wallet 1",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"mayBeFauceting": false
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"name": "Wallet 2",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b",
"mayBeFauceting": false
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"name": "Wallet 3",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823",
"mayBeFauceting": false
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"name": "Wallet 4",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"accounts": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"code": "0x",
"balance": "0x01",
"nonce": "0x0",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"code": "0x",
"nonce": "0x0",
"balance": "0x01",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"code": "0x",
"nonce": "0x0",
"balance": "0x01",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823"
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69"
}
},
"transactions": [],
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"detailView": null,
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
var assert = require('assert')
var sinon = require('sinon')
var path = require('path')
var contractNamer = require(path.join(__dirname, '..', '..', 'ui', 'lib', 'contract-namer.js'))
describe('contractNamer', function() {
beforeEach(function() {
this.sinon = sinon.sandbox.create()
})
afterEach(function() {
this.sinon.restore()
})
describe('naming a contract', function() {
it('should return nothing for an unknown random account', function() {
const input = '0x2386F26FC10000'
const output = contractNamer(input)
assert.deepEqual(output, null)
})
it('should accept identities as an optional second parameter', function() {
const input = '0x2386F26FC10000'.toLowerCase()
const expected = 'bar'
const identities = {}
identities[input] = { name: expected }
const output = contractNamer(input, identities)
assert.deepEqual(output, expected)
})
it('should check for identities case insensitively', function() {
const input = '0x2386F26FC10000'.toLowerCase()
const expected = 'bar'
const identities = {}
identities[input] = { name: expected }
const output = contractNamer(input.toUpperCase(), identities)
assert.deepEqual(output, expected)
})
})
})

View File

@ -52,6 +52,12 @@ describe('util', function() {
var result = util.addressSummary(address) var result = util.addressSummary(address)
assert.equal(result, '0xFDEa65C8...b825') assert.equal(result, '0xFDEa65C8...b825')
}) })
it('should accept arguments for firstseg, lastseg, and keepPrefix', function() {
var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'
var result = util.addressSummary(address, 4, 4, false)
assert.equal(result, 'FDEa...b825')
})
}) })
describe('isValidAddress', function() { describe('isValidAddress', function() {

View File

@ -1,3 +1,20 @@
/* UI DEV
*
* This is a utility module.
* It initializes a minimalist browserifiable project
* that contains the Metamask UI, with a mocked state.
*
* Includes a state menu for switching between different
* mocked states, along with query param support,
* so those states are preserved when live-reloading.
*
* This is a convenient way to develop on the UI
* without having to re-enter your password
* every time the plugin rebuilds.
*
* To use, run `npm run ui`.
*/
const render = require('react-dom').render const render = require('react-dom').render
const h = require('react-hyperscript') const h = require('react-hyperscript')
const Root = require('./ui/app/root') const Root = require('./ui/app/root')
@ -54,7 +71,7 @@ render(
style: { style: {
height: '500px', height: '500px',
width: '360px', width: '360px',
boxShadow: '2px 2px 5px grey', boxShadow: 'grey 0px 2px 9px',
margin: '20px', margin: '20px',
}, },
}, [ }, [

View File

@ -22,7 +22,7 @@ AccountPanel.prototype.render = function () {
var panelState = { var panelState = {
key: `accountPanel${identity.address}`, key: `accountPanel${identity.address}`,
identiconKey: identity.address, identiconKey: identity.address,
identiconLabel: identity.name, identiconLabel: identity.name || '',
attributes: [ attributes: [
{ {
key: 'ADDRESS', key: 'ADDRESS',

View File

@ -12,9 +12,11 @@ function EthBalanceComponent () {
} }
EthBalanceComponent.prototype.render = function () { EthBalanceComponent.prototype.render = function () {
var state = this.props var props = this.props
var style = state.style var style = props.style
var value = formatBalance(state.value)
const value = formatBalance(props.value)
return ( return (
h('.ether-balance', { h('.ether-balance', {
@ -30,30 +32,37 @@ EthBalanceComponent.prototype.render = function () {
) )
} }
EthBalanceComponent.prototype.renderBalance = function (value) { EthBalanceComponent.prototype.renderBalance = function (value) {
const props = this.props
if (value === 'None') return value if (value === 'None') return value
var balanceObj = generateBalanceObject(value) var balanceObj = generateBalanceObject(value)
var balance = balanceObj.balance var balance = balanceObj.balance
var label = balanceObj.label var label = balanceObj.label
var tagName = props.inline ? 'span' : 'div'
var topTag = props.inline ? 'div' : '.flex-column'
return ( return (
h(Tooltip, { h(Tooltip, {
position: 'bottom', position: 'bottom',
title: value.split(' ')[0], title: value.split(' ')[0],
}, [ }, [
h('.flex-column', { h(topTag, {
style: { style: {
alignItems: 'flex-end', alignItems: 'flex-end',
lineHeight: '13px', lineHeight: props.fontSize || '13px',
fontFamily: 'Montserrat Light', fontFamily: 'Montserrat Regular',
textRendering: 'geometricPrecision', textRendering: 'geometricPrecision',
}, },
}, [ }, [
h('div', balance), h(tagName, {
h('div', {
style: { style: {
color: ' #AEAEAE', fontSize: props.fontSize || '12px',
fontSize: '12px', },
}, balance + ' '),
h(tagName, {
style: {
color: props.labelColor || '#AEAEAE',
fontSize: props.fontSize || '12px',
}, },
}, label), }, label),
]), ]),

View File

@ -0,0 +1,74 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const Identicon = require('./identicon')
module.exports = AccountPanel
inherits(AccountPanel, Component)
function AccountPanel () {
Component.call(this)
}
AccountPanel.prototype.render = function () {
var props = this.props
var picOrder = props.picOrder || 'left'
const { imageSeed } = props
return (
h('.identity-panel.flex-row.flex-left', {
style: {
cursor: props.onClick ? 'pointer' : undefined,
},
onClick: props.onClick,
}, [
this.genIcon(imageSeed, picOrder),
h('div.flex-column.flex-justify-center', {
style: {
lineHeight: '15px',
order: 2,
display: 'flex',
alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end',
},
}, this.props.children),
])
)
}
AccountPanel.prototype.genIcon = function (seed, picOrder) {
const props = this.props
// When there is no seed value, this is a contract creation.
// We then show the contract icon.
if (!seed) {
return h('.identicon-wrapper.flex-column.select-none', {
style: {
order: picOrder === 'left' ? 1 : 3,
},
}, [
h('i.fa.fa-file-text-o.fa-lg', {
style: {
fontSize: '42px',
transform: 'translate(0px, -16px)',
},
}),
])
}
// If there was a seed, we return an identicon for that address.
return h('.identicon-wrapper.flex-column.select-none', {
style: {
order: picOrder === 'left' ? 1 : 3,
},
}, [
h(Identicon, {
address: seed,
imageify: props.imageifyIdenticons,
}),
])
}

View File

@ -2,10 +2,17 @@ const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const AccountPanel = require('./account-panel') const MiniAccountPanel = require('./mini-account-panel')
const EtherBalance = require('./eth-balance')
const addressSummary = require('../util').addressSummary const addressSummary = require('../util').addressSummary
const readableDate = require('../util').readableDate
const formatBalance = require('../util').formatBalance const formatBalance = require('../util').formatBalance
const nameForAddress = require('../../lib/contract-namer')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const baseGasFee = new BN('21000', 10)
const gasCost = new BN('4a817c800', 16)
const baseFeeHex = baseGasFee.mul(gasCost).toString(16)
module.exports = PendingTxDetails module.exports = PendingTxDetails
@ -14,52 +21,204 @@ function PendingTxDetails () {
Component.call(this) Component.call(this)
} }
PendingTxDetails.prototype.render = function () { const PTXP = PendingTxDetails.prototype
var state = this.props
return this.renderGeneric(h, state)
}
PendingTxDetails.prototype.renderGeneric = function (h, state) { PTXP.render = function () {
var txData = state.txData var props = this.props
var txData = props.txData
var txParams = txData.txParams || {} var txParams = txData.txParams || {}
var address = txParams.from || state.selectedAddress var address = txParams.from || props.selectedAddress
var identity = state.identities[address] || { address: address } var identity = props.identities[address] || { address: address }
var account = state.accounts[address] || { address: address } var balance = props.accounts[address].balance
var gasCost = ethUtil.stripHexPrefix(txParams.gas || baseFeeHex)
var txValue = ethUtil.stripHexPrefix(txParams.value || '0x0')
var maxCost = ((new BN(txValue, 16)).add(new BN(gasCost, 16))).toString(16)
var dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0
return ( return (
h('div', [ h('div', [
// account that will sign h('.flex-row.flex-center', {
h(AccountPanel, { style: {
showFullAddress: true, maxWidth: '100%',
identity: identity, },
account: account, }, [
imageifyIdenticons: state.imageifyIdenticons,
h(MiniAccountPanel, {
imageSeed: address,
imageifyIdenticons: props.imageifyIdenticons,
picOrder: 'right',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, identity.name),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, addressSummary(address, 6, 4, false)),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, h(EtherBalance, {
value: balance,
inline: true,
})),
]),
h('img', {
src: 'images/forward-carrat.svg',
style: {
padding: '5px 6px 0px 10px',
height: '37px',
},
}), }),
// tx data this.miniAccountPanelForRecipient(),
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'TO ADDRESS'),
h('span.font-small', addressSummary(txParams.to)),
]), ]),
h('.flex-row.flex-space-between', [ h('style', `
h('label.font-small', 'DATE'), .table-box {
h('span.font-small', readableDate(txData.time)), margin: 7px 0px 0px 0px;
width: 100%;
}
.table-box .row {
margin: 0px;
background: rgb(236,236,236);
display: flex;
justify-content: space-between;
font-family: Montserrat Light, sans-serif;
font-size: 13px;
padding: 5px 25px;
}
.table-box .row .value {
font-family: Montserrat Regular;
}
`),
h('.table-box', [
h('.row', [
h('.cell.label', 'Amount'),
h('.cell.value', formatBalance(txParams.value)),
]), ]),
h('.flex-row.flex-space-between', [ h('.cell.row', [
h('label.font-small', 'AMOUNT'), h('.cell.label', 'Max Transaction Fee'),
h('span.font-small', formatBalance(txParams.value)), h('.cell.value', formatBalance(gasCost)),
]),
h('.cell.row', {
style: {
fontFamily: 'Montserrat Regular',
background: 'white',
padding: '10px 25px',
},
}, [
h('.cell.label', 'Max Total'),
h('.cell.value', {
style: {
display: 'flex',
alignItems: 'center',
},
}, [
h(EtherBalance, {
value: maxCost,
inline: true,
labelColor: 'black',
fontSize: '16px',
}),
]), ]),
]), ]),
h('.cell.row', {
style: {
background: '#f7f7f7',
paddingBottom: '0px',
},
}, [
h('.cell.label'),
h('.cell.value', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '11px',
},
}, `Data included: ${dataLength} bytes`),
]),
]), // End of Table
this.warnIfNeeded(),
]) ])
) )
}
PTXP.miniAccountPanelForRecipient = function () {
var props = this.props
var txData = props.txData
var txParams = txData.txParams || {}
var isContractDeploy = !('to' in txParams)
// If it's not a contract deploy, send to the account
if (!isContractDeploy) {
return h(MiniAccountPanel, {
imageSeed: txParams.to,
imageifyIdenticons: props.imageifyIdenticons,
picOrder: 'left',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, nameForAddress(txParams.to, props.identities)),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, addressSummary(txParams.to, 6, 4, false)),
])
} else {
return h(MiniAccountPanel, {
imageifyIdenticons: props.imageifyIdenticons,
picOrder: 'left',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, 'New Contract'),
])
}
}
// Should analyze if there is a DELEGATECALL opcode
// in the recipient contract, and show a warning if so.
PTXP.warnIfNeeded = function () {
const containsDelegateCall = !!this.props.txData.containsDelegateCall
if (!containsDelegateCall) {
return null
}
return h('span.error', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
display: 'flex',
justifyContent: 'center',
},
}, [
h('i.fa.fa-lg.fa-info-circle', { style: { margin: '5px' } }),
h('span', ' Your identity may be used in other contracts!'),
])
} }

View File

@ -21,29 +21,35 @@ PendingTx.prototype.render = function () {
key: txData.id, key: txData.id,
}, [ }, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, 'Submit Transaction'),
// tx info // tx info
h(PendingTxDetails, state), h(PendingTxDetails, state),
// send + cancel h('style', `
h('.flex-row.flex-space-around', [ .conf-buttons button {
h('button', { margin-left: 10px;
onClick: state.cancelTransaction, text-transform: uppercase;
}, 'Reject'),
h('button', {
onClick: state.sendTransaction,
}, 'Approve'),
]),
])
)
} }
`),
// send + cancel
h('.flex-row.flex-space-around.conf-buttons', {
style: {
display: 'flex',
justifyContent: 'flex-end',
margin: '14px 25px',
},
}, [
h('button.confirm', {
onClick: state.sendTransaction,
style: { background: 'rgb(251,117,1)' },
}, 'Accept'),
h('button.cancel', {
onClick: state.cancelTransaction,
style: { background: 'rgb(254,35,17)' },
}, 'Reject'),
]),
])
)
}

View File

@ -39,14 +39,14 @@ ConfirmTxScreen.prototype.render = function () {
return ( return (
h('.unconftx-section.flex-column.flex-grow', [ h('.flex-column.flex-grow', [
// subtitle and nav // subtitle and nav
h('.section-title.flex-row.flex-center', [ h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.goHome.bind(this), onClick: this.goHome.bind(this),
}), }),
h('h2.page-subtitle', 'Confirmation'), h('h2.page-subtitle', 'Confirm Transaction'),
]), ]),
h('h3', { h('h3', {

View File

@ -411,10 +411,6 @@ input.large-input {
} }
/* tx confirm */ /* tx confirm */
.unconftx-section {
margin: 0 20px;
}
.unconftx-section input[type=password] { .unconftx-section input[type=password] {
height: 22px; height: 22px;
padding: 2px; padding: 2px;

View File

@ -220,3 +220,9 @@ hr.horizontal-line {
.invisible { .invisible {
visibility: hidden; visibility: hidden;
} }
.one-line-concat {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -21,6 +21,7 @@ for (var currency in valueTable) {
module.exports = { module.exports = {
valuesFor: valuesFor, valuesFor: valuesFor,
addressSummary: addressSummary, addressSummary: addressSummary,
miniAddressSummary: miniAddressSummary,
isAllOneCase: isAllOneCase, isAllOneCase: isAllOneCase,
isValidAddress: isValidAddress, isValidAddress: isValidAddress,
numericBalance: numericBalance, numericBalance: numericBalance,
@ -44,10 +45,19 @@ function valuesFor (obj) {
.map(function (key) { return obj[key] }) .map(function (key) { return obj[key] })
} }
function addressSummary (address) { function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) {
if (!address) return ''
let checked = ethUtil.toChecksumAddress(address)
if (!includeHex) {
checked = ethUtil.stripHexPrefix(checked)
}
return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...'
}
function miniAddressSummary (address) {
if (!address) return '' if (!address) return ''
var checked = ethUtil.toChecksumAddress(address) var checked = ethUtil.toChecksumAddress(address)
return checked ? checked.slice(0, 2 + 8) + '...' + checked.slice(-4) : '...' return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...'
} }
function isValidAddress (address) { function isValidAddress (address) {
@ -95,7 +105,8 @@ function parseBalance (balance) {
return [beforeDecimal, afterDecimal] return [beforeDecimal, afterDecimal]
} }
// Takes wei hex, returns "None" or "${formattedAmount} ETH" // Takes wei hex, returns an object with three properties.
// Its "formatted" property is what we generally use to render values.
function formatBalance (balance, decimalsToKeep) { function formatBalance (balance, decimalsToKeep) {
var parsed = parseBalance(balance) var parsed = parseBalance(balance)
var beforeDecimal = parsed[0] var beforeDecimal = parsed[0]

31
ui/lib/contract-namer.js Normal file
View File

@ -0,0 +1,31 @@
/* CONTRACT NAMER
*
* Takes an address,
* Returns a nicname if we have one stored,
* otherwise returns null.
*/
// Nickname keys must be stored in lower case.
const nicknames = {}
module.exports = function(addr, identities = {}) {
const address = addr.toLowerCase()
const ids = hashFromIdentities(identities)
console.dir({ addr, ids })
return addrFromHash(address, ids) || addrFromHash(address, nicknames)
}
function hashFromIdentities(identities) {
const result = {}
for (let key in identities) {
result[key] = identities[key].name
}
return result
}
function addrFromHash(addr, hash) {
const address = addr.toLowerCase()
return hash[address] || null
}