[beta] UI backports (#4809)

* Update Wallet to new Wallet Code (#4805)

* Update Wallet Version

* Update Wallet Library

* Update Wallets Bytecodes

* Typo

* Separate Deploy in Contract API

* Use the new Wallet ABI // Update wallet code

* WIP .// Deploy from Wallet

* Update Wallet contract

* Contract Deployment for Wallet

* Working deployments for Single Owned Wallet contracts

* Linting

* Create a Wallet from a Wallet

* Linting

* Fix Signer transactions // Add Gas Used for transactions

* Deploy wallet contract fix

* Fix too high gas estimate for Wallet Contract Deploys

* Final piece ; deploying from Wallet owned by wallet

* Update Wallet Code

* Updated the Wallet Codes

* Fixing Wallet Deployments

* Add Support for older wallets

* Linting

* SMS Faucet (#4774)

* Faucet

* Remove flakey button-index testing

* Only display faucet when sms verified (mainnet)

* simplify availability checks

* WIP

* Resuest from verified -> verified

* Update endpoint, display response text

* Error icon on errors

* Parse hash text response

* Use /api/:address endpoint

* hash -> data

* Adjust sms-certified message

* Fix SectionList hovering issue (#4749)

* Fix SectionList Items hover when <3 items

* Even easier...

* lint (new)
This commit is contained in:
Jaco Greeff 2017-03-08 10:43:59 +01:00 committed by Arkadiy Paronyan
parent 3b56e8eded
commit c4196a5de3
32 changed files with 2656 additions and 993 deletions

View File

@ -107,34 +107,26 @@ export default class Contract {
}); });
} }
deploy (options, values, statecb) { deploy (options, values, statecb = () => {}) {
const setState = (state) => { statecb(null, { state: 'estimateGas' });
if (!statecb) {
return;
}
return statecb(null, state);
};
setState({ state: 'estimateGas' });
return this return this
.deployEstimateGas(options, values) .deployEstimateGas(options, values)
.then(([gasEst, gas]) => { .then(([gasEst, gas]) => {
options.gas = gas.toFixed(0); options.gas = gas.toFixed(0);
setState({ state: 'postTransaction', gas }); statecb(null, { state: 'postTransaction', gas });
const _options = this._encodeOptions(this.constructors[0], options, values); const encodedOptions = this._encodeOptions(this.constructors[0], options, values);
return this._api.parity return this._api.parity
.postTransaction(_options) .postTransaction(encodedOptions)
.then((requestId) => { .then((requestId) => {
setState({ state: 'checkRequest', requestId }); statecb(null, { state: 'checkRequest', requestId });
return this._pollCheckRequest(requestId); return this._pollCheckRequest(requestId);
}) })
.then((txhash) => { .then((txhash) => {
setState({ state: 'getTransactionReceipt', txhash }); statecb(null, { state: 'getTransactionReceipt', txhash });
return this._pollTransactionReceipt(txhash, gas); return this._pollTransactionReceipt(txhash, gas);
}) })
.then((receipt) => { .then((receipt) => {
@ -142,14 +134,13 @@ export default class Contract {
throw new Error(`Contract not deployed, gasUsed == ${gas.toFixed(0)}`); throw new Error(`Contract not deployed, gasUsed == ${gas.toFixed(0)}`);
} }
setState({ state: 'hasReceipt', receipt }); statecb(null, { state: 'hasReceipt', receipt });
this._receipt = receipt; this._receipt = receipt;
this._address = receipt.contractAddress; this._address = receipt.contractAddress;
return this._address; return this._address;
});
}) })
.then((address) => { .then((address) => {
setState({ state: 'getCode' }); statecb(null, { state: 'getCode' });
return this._api.eth.getCode(this._address); return this._api.eth.getCode(this._address);
}) })
.then((code) => { .then((code) => {
@ -157,9 +148,10 @@ export default class Contract {
throw new Error('Contract not deployed, getCode returned 0x'); throw new Error('Contract not deployed, getCode returned 0x');
} }
setState({ state: 'completed' }); statecb(null, { state: 'completed' });
return this._address; return this._address;
}); });
});
} }
parseEventLogs (logs) { parseEventLogs (logs) {

View File

@ -0,0 +1,466 @@
[
{
"constant": false,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "removeOwner",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_addr",
"type": "address"
}
],
"name": "isOwner",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_numOwners",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_lastDay",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "resetSpentToday",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_spentToday",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "addOwner",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_required",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_h",
"type": "bytes32"
}
],
"name": "confirm",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_newLimit",
"type": "uint256"
}
],
"name": "setDailyLimit",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
},
{
"name": "_data",
"type": "bytes"
}
],
"name": "execute",
"outputs": [
{
"name": "_r",
"type": "bytes32"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_operation",
"type": "bytes32"
}
],
"name": "revoke",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_newRequired",
"type": "uint256"
}
],
"name": "changeRequirement",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_operation",
"type": "bytes32"
},
{
"name": "_owner",
"type": "address"
}
],
"name": "hasConfirmed",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "ownerIndex",
"type": "uint256"
}
],
"name": "getOwner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
}
],
"name": "kill",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
}
],
"name": "changeOwner",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_dailyLimit",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"inputs": [
{
"name": "_owners",
"type": "address[]"
},
{
"name": "_required",
"type": "uint256"
},
{
"name": "_daylimit",
"type": "uint256"
}
],
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "operation",
"type": "bytes32"
}
],
"name": "Confirmation",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "operation",
"type": "bytes32"
}
],
"name": "Revoke",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "oldOwner",
"type": "address"
},
{
"indexed": false,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "oldOwner",
"type": "address"
}
],
"name": "OwnerRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "newRequirement",
"type": "uint256"
}
],
"name": "RequirementChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "_from",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
}
],
"name": "SingleTransact",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "operation",
"type": "bytes32"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
}
],
"name": "MultiTransact",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "operation",
"type": "bytes32"
},
{
"indexed": false,
"name": "initiator",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
}
],
"name": "ConfirmationNeeded",
"type": "event"
}
]

View File

@ -1 +1,476 @@
[{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"removeOwner","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"m_numOwners","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"m_lastDay","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"resetSpentToday","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_spentToday","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"addOwner","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_required","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_h","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_newLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"_r","type":"bytes32"}],"type":"function"},{"constant":false,"inputs":[{"name":"_operation","type":"bytes32"}],"name":"revoke","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_newRequired","type":"uint256"}],"name":"changeRequirement","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_operation","type":"bytes32"},{"name":"_owner","type":"address"}],"name":"hasConfirmed","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"ownerIndex","type":"uint256"}],"name":"getOwner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"}],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"name":"changeOwner","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_dailyLimit","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"},{"name":"_daylimit","type":"uint256"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Confirmation","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Revoke","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"},{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"}],"name":"OwnerRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newRequirement","type":"uint256"}],"name":"RequirementChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"SingleTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"MultiTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"initiator","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"ConfirmationNeeded","type":"event"}] [
{
"constant": false,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "removeOwner",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_addr",
"type": "address"
}
],
"name": "isOwner",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_numOwners",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_lastDay",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "resetSpentToday",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_spentToday",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "addOwner",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_required",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_h",
"type": "bytes32"
}
],
"name": "confirm",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_newLimit",
"type": "uint256"
}
],
"name": "setDailyLimit",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
},
{
"name": "_data",
"type": "bytes"
}
],
"name": "execute",
"outputs": [
{
"name": "_r",
"type": "bytes32"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_operation",
"type": "bytes32"
}
],
"name": "revoke",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_newRequired",
"type": "uint256"
}
],
"name": "changeRequirement",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_operation",
"type": "bytes32"
},
{
"name": "_owner",
"type": "address"
}
],
"name": "hasConfirmed",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "ownerIndex",
"type": "uint256"
}
],
"name": "getOwner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
}
],
"name": "kill",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
}
],
"name": "changeOwner",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "m_dailyLimit",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
},
{
"inputs": [
{
"name": "_owners",
"type": "address[]"
},
{
"name": "_required",
"type": "uint256"
},
{
"name": "_daylimit",
"type": "uint256"
}
],
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "operation",
"type": "bytes32"
}
],
"name": "Confirmation",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "operation",
"type": "bytes32"
}
],
"name": "Revoke",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "oldOwner",
"type": "address"
},
{
"indexed": false,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "oldOwner",
"type": "address"
}
],
"name": "OwnerRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "newRequirement",
"type": "uint256"
}
],
"name": "RequirementChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "_from",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
},
{
"indexed": false,
"name": "created",
"type": "address"
}
],
"name": "SingleTransact",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "owner",
"type": "address"
},
{
"indexed": false,
"name": "operation",
"type": "bytes32"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
},
{
"indexed": false,
"name": "created",
"type": "address"
}
],
"name": "MultiTransact",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "operation",
"type": "bytes32"
},
{
"indexed": false,
"name": "initiator",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
}
],
"name": "ConfirmationNeeded",
"type": "event"
}
]

File diff suppressed because one or more lines are too long

View File

@ -8,12 +8,13 @@
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed. // interior is executed.
pragma solidity ^0.4.6;
contract multisig { pragma solidity ^0.4.9;
contract WalletEvents {
// EVENTS // EVENTS
// this contract can accept a confirmation, in which case // this contract only has six types of events: it can accept a confirmation, in which case
// we record owner and operation (hash) alongside it. // we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation); event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation); event Revoke(address owner, bytes32 operation);
@ -29,38 +30,38 @@ contract multisig {
// Funds has arrived into the wallet (record how much). // Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value); event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data); event SingleTransact(address owner, uint value, address to, bytes data, address created);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created);
// Confirmation still needed for a transaction. // Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
} }
contract multisigAbi is multisig { contract WalletAbi {
function isOwner(address _addr) returns (bool); // Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external;
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool); // Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) external;
function confirm(bytes32 _h) returns(bool); function addOwner(address _owner) external;
function removeOwner(address _owner) external;
function changeRequirement(uint _newRequired) external;
function isOwner(address _addr) constant returns (bool);
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool);
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit); function setDailyLimit(uint _newLimit) external;
function addOwner(address _owner); function execute(address _to, uint _value, bytes _data) external returns (bytes32 o_hash);
function confirm(bytes32 _h) returns (bool o_success);
function removeOwner(address _owner);
function changeRequirement(uint _newRequired);
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation);
function changeOwner(address _from, address _to);
function execute(address _to, uint _value, bytes _data) returns(bool);
} }
contract WalletLibrary is multisig { contract WalletLibrary is WalletEvents {
// TYPES // TYPES
// struct for the status of a pending operation. // struct for the status of a pending operation.
@ -77,10 +78,6 @@ contract WalletLibrary is multisig {
bytes data; bytes data;
} }
/******************************
***** MULTI OWNED SECTION ****
******************************/
// MODIFIERS // MODIFIERS
// simple single-sig function modifier. // simple single-sig function modifier.
@ -98,23 +95,29 @@ contract WalletLibrary is multisig {
// METHODS // METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
}
// constructor is given number of sigs required to do protected "onlymanyowners" transactions // constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them. // as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] _owners, uint _required) { function initMultiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1; m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender); m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1; m_ownerIndex[uint(msg.sender)] = 1;
m_required = _required;
for (uint i = 0; i < _owners.length; ++i) for (uint i = 0; i < _owners.length; ++i)
{ {
m_owners[2 + i] = uint(_owners[i]); m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i; m_ownerIndex[uint(_owners[i])] = 2 + i;
} }
m_required = _required;
} }
// Revokes a prior confirmation of the given operation // Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) { function revoke(bytes32 _operation) external {
uint ownerIndex = m_ownerIndex[uint(msg.sender)]; uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner // make sure they're an owner
if (ownerIndex == 0) return; if (ownerIndex == 0) return;
@ -128,7 +131,7 @@ contract WalletLibrary is multisig {
} }
// Replaces an owner `_from` with another `_to`. // Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) { function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_to)) return; if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)]; uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return; if (ownerIndex == 0) return;
@ -140,7 +143,7 @@ contract WalletLibrary is multisig {
OwnerChanged(_from, _to); OwnerChanged(_from, _to);
} }
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) { function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_owner)) return; if (isOwner(_owner)) return;
clearPending(); clearPending();
@ -154,7 +157,7 @@ contract WalletLibrary is multisig {
OwnerAdded(_owner); OwnerAdded(_owner);
} }
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) { function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)]; uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return; if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return; if (m_required > m_numOwners - 1) return;
@ -166,19 +169,23 @@ contract WalletLibrary is multisig {
OwnerRemoved(_owner); OwnerRemoved(_owner);
} }
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) { function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
if (_newRequired > m_numOwners) return; if (_newRequired > m_numOwners) return;
m_required = _newRequired; m_required = _newRequired;
clearPending(); clearPending();
RequirementChanged(_newRequired); RequirementChanged(_newRequired);
} }
function isOwner(address _addr) returns (bool) { // Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) external constant returns (address) {
return address(m_owners[ownerIndex + 1]);
}
function isOwner(address _addr) constant returns (bool) {
return m_ownerIndex[uint(_addr)] > 0; return m_ownerIndex[uint(_addr)] > 0;
} }
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation]; var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)]; uint ownerIndex = m_ownerIndex[uint(_owner)];
@ -190,6 +197,88 @@ contract WalletLibrary is multisig {
return !(pending.ownersDone & ownerIndexBit == 0); return !(pending.ownersDone & ownerIndexBit == 0);
} }
// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(sha3(msg.data)) external {
m_spentToday = 0;
}
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) {
// first, take the opportunity to check that we're under the daily limit.
if ((_data.length == 0 && underLimit(_value)) || m_required == 1) {
// yes - just execute the call.
address created;
if (_to == 0) {
created = create(_value, _data);
} else {
if (!_to.call.value(_value)(_data))
throw;
}
SingleTransact(msg.sender, _value, _to, _data, created);
} else {
// determine our operation hash.
o_hash = sha3(msg.data, block.number);
// store if it's new
if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) {
m_txs[o_hash].to = _to;
m_txs[o_hash].value = _value;
m_txs[o_hash].data = _data;
}
if (!confirm(o_hash)) {
ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data);
}
}
}
function create(uint _value, bytes _code) internal returns (address o_addr) {
assembly {
o_addr := create(_value, add(_code, 0x20), mload(_code))
jumpi(invalidJumpLabel, iszero(extcodesize(o_addr)))
}
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) {
if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) {
address created;
if (m_txs[_h].to == 0) {
created = create(m_txs[_h].value, m_txs[_h].data);
} else {
if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data))
throw;
}
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS // INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) { function confirmAndCheck(bytes32 _operation) internal returns (bool) {
@ -244,45 +333,6 @@ contract WalletLibrary is multisig {
} }
} }
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
delete m_pendingIndex;
}
/******************************
****** DAY LIMIT SECTION *****
******************************/
// MODIFIERS
// simple modifier for daily limit.
modifier limitedDaily(uint _value) {
if (underLimit(_value))
_;
}
// METHODS
// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(sha3(msg.data)) {
m_spentToday = 0;
}
// INTERNAL METHODS
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and
// returns true. otherwise just returns false. // returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) { function underLimit(uint _value) internal onlyowner returns (bool) {
@ -303,74 +353,26 @@ contract WalletLibrary is multisig {
// determines today's index. // determines today's index.
function today() private constant returns (uint) { return now / 1 days; } function today() private constant returns (uint) { return now / 1 days; }
function clearPending() internal {
/******************************
********* WALLET SECTION *****
******************************/
// METHODS
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initMultiowned(_owners, _required);
initDaylimit(_daylimit) ;
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) {
suicide(_to);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) onlyowner returns(bool _callValue) {
// first, take the opportunity to check that we're under the daily limit.
if (underLimit(_value)) {
SingleTransact(msg.sender, _value, _to, _data);
// yes - just execute the call.
_callValue =_to.call.value(_value)(_data);
} else {
// determine our operation hash.
bytes32 _r = sha3(msg.data, block.number);
if (!confirm(_r) && m_txs[_r].to == 0) {
m_txs[_r].to = _to;
m_txs[_r].value = _value;
m_txs[_r].data = _data;
ConfirmationNeeded(_r, msg.sender, _value, _to, _data);
}
}
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) {
if (m_txs[_h].to != 0) {
m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data);
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS
function clearWalletPending() internal {
uint length = m_pendingIndex.length; uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
for (uint i = 0; i < length; ++i) {
delete m_txs[m_pendingIndex[i]]; delete m_txs[m_pendingIndex[i]];
clearPending();
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
}
delete m_pendingIndex;
} }
// FIELDS // FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run. // the number of owners that must confirm the same operation before it is run.
uint m_required; uint public m_required;
// pointer used to find a free slot in m_owners // pointer used to find a free slot in m_owners
uint m_numOwners; uint public m_numOwners;
uint public m_dailyLimit; uint public m_dailyLimit;
uint public m_spentToday; uint public m_spentToday;
@ -378,8 +380,8 @@ contract WalletLibrary is multisig {
// list of owners // list of owners
uint[256] m_owners; uint[256] m_owners;
uint constant c_maxOwners = 250;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup // index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex; mapping(uint => uint) m_ownerIndex;
// the ongoing operations. // the ongoing operations.
@ -390,8 +392,7 @@ contract WalletLibrary is multisig {
mapping (bytes32 => Transaction) m_txs; mapping (bytes32 => Transaction) m_txs;
} }
contract Wallet is WalletEvents {
contract Wallet is multisig {
// WALLET CONSTRUCTOR // WALLET CONSTRUCTOR
// calls the `initWallet` method of the Library in this context // calls the `initWallet` method of the Library in this context
@ -435,11 +436,11 @@ contract Wallet is multisig {
// As return statement unavailable in fallback, explicit the method here // As return statement unavailable in fallback, explicit the method here
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
return _walletLibrary.delegatecall(msg.data); return _walletLibrary.delegatecall(msg.data);
} }
function isOwner(address _addr) returns (bool) { function isOwner(address _addr) constant returns (bool) {
return _walletLibrary.delegatecall(msg.data); return _walletLibrary.delegatecall(msg.data);
} }

View File

@ -8,7 +8,8 @@
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed. // interior is executed.
pragma solidity ^0.4.6;
pragma solidity ^0.4.9;
contract multiowned { contract multiowned {
@ -130,7 +131,7 @@ contract multiowned {
return address(m_owners[ownerIndex + 1]); return address(m_owners[ownerIndex + 1]);
} }
function isOwner(address _addr) returns (bool) { function isOwner(address _addr) constant returns (bool) {
return m_ownerIndex[uint(_addr)] > 0; return m_ownerIndex[uint(_addr)] > 0;
} }
@ -230,14 +231,6 @@ contract multiowned {
// uses is specified in the modifier. // uses is specified in the modifier.
contract daylimit is multiowned { contract daylimit is multiowned {
// MODIFIERS
// simple modifier for daily limit.
modifier limitedDaily(uint _value) {
if (underLimit(_value))
_;
}
// METHODS // METHODS
// constructor - stores initial daily limit and records the present day's index. // constructor - stores initial daily limit and records the present day's index.
@ -291,18 +284,17 @@ contract multisig {
// Funds has arrived into the wallet (record how much). // Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value); event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data); event SingleTransact(address owner, uint value, address to, bytes data, address created);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created);
// Confirmation still needed for a transaction. // Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
// FUNCTIONS // FUNCTIONS
// TODO: document // TODO: document
function changeOwner(address _from, address _to) external; function execute(address _to, uint _value, bytes _data) external returns (bytes32 o_hash);
function execute(address _to, uint _value, bytes _data) external returns (bytes32); function confirm(bytes32 _h) external returns (bool o_success);
function confirm(bytes32 _h) returns (bool);
} }
// usage: // usage:
@ -343,30 +335,53 @@ contract Wallet is multisig, multiowned, daylimit {
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide // If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways. // and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 _r) { function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) {
// first, take the opportunity to check that we're under the daily limit. // first, take the opportunity to check that we're under the daily limit.
if (underLimit(_value)) { if ((_data.length == 0 && underLimit(_value)) || m_required == 1) {
SingleTransact(msg.sender, _value, _to, _data);
// yes - just execute the call. // yes - just execute the call.
_to.call.value(_value)(_data); address created;
return 0; if (_to == 0) {
created = create(_value, _data);
} else {
if (!_to.call.value(_value)(_data))
throw;
} }
SingleTransact(msg.sender, _value, _to, _data, created);
} else {
// determine our operation hash. // determine our operation hash.
_r = sha3(msg.data, block.number); o_hash = sha3(msg.data, block.number);
if (!confirm(_r) && m_txs[_r].to == 0) { // store if it's new
m_txs[_r].to = _to; if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) {
m_txs[_r].value = _value; m_txs[o_hash].to = _to;
m_txs[_r].data = _data; m_txs[o_hash].value = _value;
ConfirmationNeeded(_r, msg.sender, _value, _to, _data); m_txs[o_hash].data = _data;
}
if (!confirm(o_hash)) {
ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data);
}
}
}
function create(uint _value, bytes _code) internal returns (address o_addr) {
assembly {
o_addr := create(_value, add(_code, 0x20), mload(_code))
jumpi(invalidJumpLabel, iszero(extcodesize(o_addr)))
} }
} }
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided. // to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) {
if (m_txs[_h].to != 0) { if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) {
m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); address created;
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data); if (m_txs[_h].to == 0) {
created = create(m_txs[_h].value, m_txs[_h].data);
} else {
if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data))
throw;
}
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created);
delete m_txs[_h]; delete m_txs[_h];
return true; return true;
} }

View File

@ -14,7 +14,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { omitBy } from 'lodash';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -106,9 +105,6 @@ export default class WalletDetails extends Component {
renderMultisigDetails () { renderMultisigDetails () {
const { accounts, wallet, errors } = this.props; const { accounts, wallet, errors } = this.props;
// Wallets cannot create contracts
const _accounts = omitBy(accounts, (a) => a.wallet);
return ( return (
<Form> <Form>
<Input <Input
@ -148,7 +144,7 @@ export default class WalletDetails extends Component {
/> />
<AddressSelect <AddressSelect
accounts={ _accounts } accounts={ accounts }
error={ errors.account } error={ errors.account }
hint={ hint={
<FormattedMessage <FormattedMessage

View File

@ -22,10 +22,11 @@ import Contract from '~/api/contract';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { wallet as walletAbi } from '~/contracts/abi'; import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; import { wallet as walletCode, walletLibrary as walletLibraryCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
import { validateUint, validateAddress, validateName } from '~/util/validation'; import { validateUint, validateAddress, validateName } from '~/util/validation';
import { toWei } from '~/api/util/wei'; import { toWei } from '~/api/util/wei';
import { deploy } from '~/util/tx';
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
const STEPS = { const STEPS = {
@ -179,6 +180,8 @@ export default class CreateWalletStore {
this.wallet.owners = owners; this.wallet.owners = owners;
this.wallet.required = require.toNumber(); this.wallet.required = require.toNumber();
this.wallet.dailylimit = dailylimit.limit; this.wallet.dailylimit = dailylimit.limit;
this.wallet = this.getWalletWithMeta(this.wallet);
}); });
return this.addWallet(this.wallet); return this.addWallet(this.wallet);
@ -202,21 +205,51 @@ export default class CreateWalletStore {
return null; // exception when registry is not available return null; // exception when registry is not available
}) })
.then((address) => { .then((address) => {
const walletLibraryAddress = (address || '').replace(/^0x/, '').toLowerCase(); if (!address || /^(0x)?0*$/.test(address)) {
const code = walletLibraryAddress.length && !/^0+$/.test(walletLibraryAddress) return null;
? walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress) }
: fullWalletCode;
// Check that it's actually the expected code
return this.api.eth
.getCode(address)
.then((code) => {
const strippedCode = code.replace(/^0x/, '');
// The actual deployed code is included in the wallet
// library code (which might have some more data)
if (walletLibraryCode.indexOf(strippedCode) >= 0) {
return address;
}
return null;
});
})
.then((address) => {
let code = fullWalletCode;
if (address) {
const walletLibraryAddress = address.replace(/^0x/, '').toLowerCase();
code = walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress);
} else {
console.warn('wallet library has not been found in the registry');
}
const options = { const options = {
data: code, data: code,
from: account from: account
}; };
return this.api const contract = this.api.newContract(walletAbi);
.newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState); this.wallet = this.getWalletWithMeta(this.wallet);
return deploy(contract, options, [ owners, required, daylimit ], this.wallet.metadata, this.onDeploymentState);
}) })
.then((address) => { .then((address) => {
if (!address || /^(0x)?0*$/.test(address)) {
return false;
}
this.deployed = true; this.deployed = true;
this.wallet.address = address; this.wallet.address = address;
return this.addWallet(this.wallet); return this.addWallet(this.wallet);
@ -233,26 +266,37 @@ export default class CreateWalletStore {
} }
@action addWallet = (wallet) => { @action addWallet = (wallet) => {
const { address, name, description } = wallet; const { address, name, metadata } = wallet;
return Promise return Promise
.all([ .all([
this.api.parity.setAccountName(address, name), this.api.parity.setAccountName(address, name),
this.api.parity.setAccountMeta(address, { this.api.parity.setAccountMeta(address, metadata)
abi: walletAbi,
wallet: true,
timestamp: Date.now(),
deleted: false,
description,
name,
tags: ['wallet']
})
]) ])
.then(() => { .then(() => {
this.step = 'INFO'; this.step = 'INFO';
}); });
} }
getWalletWithMeta = (wallet) => {
const { name, description } = wallet;
const metadata = {
abi: walletAbi,
wallet: true,
timestamp: Date.now(),
deleted: false,
tags: [ 'wallet' ],
description,
name
};
return {
...wallet,
metadata
};
}
onDeploymentState = (error, data) => { onDeploymentState = (error, data) => {
if (error) { if (error) {
return console.error('createWallet::onDeploymentState', error); return console.error('createWallet::onDeploymentState', error);
@ -298,6 +342,15 @@ export default class CreateWalletStore {
); );
return; return;
case 'confirmationNeeded':
this.deployState = (
<FormattedMessage
id='createWallet.states.confirmationNeeded'
defaultMessage='The contract deployment needs confirmations from other owners of the Wallet'
/>
);
return;
case 'completed': case 'completed':
this.deployState = ( this.deployState = (
<FormattedMessage <FormattedMessage

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { pick, omitBy } from 'lodash'; import { pick } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -23,6 +23,7 @@ import { connect } from 'react-redux';
import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui'; import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui';
import { CancelIcon, DoneIcon } from '~/ui/Icons'; import { CancelIcon, DoneIcon } from '~/ui/Icons';
import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation';
import { deploy, deployEstimateGas } from '~/util/tx';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep'; import ParametersStep from './ParametersStep';
@ -73,7 +74,7 @@ class DeployContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
@ -422,9 +423,9 @@ class DeployContract extends Component {
from: fromAddress from: fromAddress
}; };
api const contract = api.newContract(abiParsed);
.newContract(abiParsed)
.deployEstimateGas(options, params) deployEstimateGas(contract, options, params)
.then(([gasEst, gas]) => { .then(([gasEst, gas]) => {
this.gasStore.setEstimated(gasEst.toFixed(0)); this.gasStore.setEstimated(gasEst.toFixed(0));
this.gasStore.setGas(gas.toFixed(0)); this.gasStore.setGas(gas.toFixed(0));
@ -490,6 +491,17 @@ class DeployContract extends Component {
const { api, store } = this.context; const { api, store } = this.context;
const { source } = this.props; const { source } = this.props;
const { abiParsed, code, description, name, params, fromAddress } = this.state; const { abiParsed, code, description, name, params, fromAddress } = this.state;
const metadata = {
abi: abiParsed,
contract: true,
deleted: false,
timestamp: Date.now(),
name,
description,
source
};
const options = { const options = {
data: code, data: code,
from: fromAddress from: fromAddress
@ -499,28 +511,25 @@ class DeployContract extends Component {
const contract = api.newContract(abiParsed); const contract = api.newContract(abiParsed);
contract deploy(contract, options, params, metadata, this.onDeploymentState)
.deploy(options, params, this.onDeploymentState)
.then((address) => { .then((address) => {
const blockNumber = contract._receipt // No contract address given, might need some confirmations
// from the wallet owners...
if (!address || /^(0x)?0*$/.test(address)) {
return false;
}
metadata.blockNumber = contract._receipt
? contract.receipt.blockNumber.toNumber() ? contract.receipt.blockNumber.toNumber()
: null; : null;
return Promise.all([ return Promise.all([
api.parity.setAccountName(address, name), api.parity.setAccountName(address, name),
api.parity.setAccountMeta(address, { api.parity.setAccountMeta(address, metadata)
abi: abiParsed,
contract: true,
timestamp: Date.now(),
deleted: false,
blockNumber,
description,
source
})
]) ])
.then(() => { .then(() => {
console.log(`contract deployed at ${address}`); console.log(`contract deployed at ${address}`);
this.setState({ step: 'DEPLOYMENT', address }); this.setState({ step: 'COMPLETED', address });
}); });
}) })
.catch((error) => { .catch((error) => {
@ -589,6 +598,17 @@ class DeployContract extends Component {
}); });
return; return;
case 'confirmationNeeded':
this.setState({
deployState: (
<FormattedMessage
id='deployContract.state.confirmationNeeded'
defaultMessage='The operation needs confirmations from the other owners of the contract'
/>
)
});
return;
case 'completed': case 'completed':
this.setState({ this.setState({
deployState: ( deployState: (
@ -614,17 +634,14 @@ class DeployContract extends Component {
function mapStateToProps (initState, initProps) { function mapStateToProps (initState, initProps) {
const { accounts } = initProps; const { accounts } = initProps;
// Skip Wallet accounts : they can't create Contracts const fromAddresses = Object.keys(accounts);
const _accounts = omitBy(accounts, (a) => a.wallet);
const fromAddresses = Object.keys(_accounts);
return (state) => { return (state) => {
const balances = pick(state.balances.balances, fromAddresses); const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus; const { gasLimit } = state.nodeStatus;
return { return {
accounts: _accounts, accounts,
balances, balances,
gasLimit gasLimit
}; };

View File

@ -0,0 +1,162 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { txLink } from '~/3rdparty/etherscan/links';
import { Button, ModalBox, Portal, ShortenedHash } from '~/ui';
import { CloseIcon, DialIcon, DoneIcon, ErrorIcon, SendIcon } from '~/ui/Icons';
import Store from './store';
@observer
export default class Faucet extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
netVersion: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired
}
store = new Store(this.props.netVersion, this.props.address);
render () {
const { error, isBusy, isCompleted } = this.store;
let icon = <DialIcon />;
if (isCompleted) {
icon = error
? <ErrorIcon />
: <DoneIcon />;
}
return (
<Portal
buttons={ this.renderActions() }
busy={ isBusy }
isSmallModal
onClose={ this.onClose }
open
title={
<FormattedMessage
id='faucet.title'
defaultMessage='Kovan ETH Faucet'
/>
}
>
<ModalBox
icon={ icon }
summary={
isCompleted
? this.renderSummaryDone()
: this.renderSummaryRequest()
}
/>
</Portal>
);
}
renderActions = () => {
const { canTransact, isBusy, isCompleted } = this.store;
return isCompleted || isBusy
? (
<Button
disabled={ isBusy }
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='faucet.buttons.done'
defaultMessage='close'
/>
}
onClick={ this.onClose }
/>
)
: [
<Button
icon={ <CloseIcon /> }
key='close'
label={
<FormattedMessage
id='faucet.buttons.close'
defaultMessage='close'
/>
}
onClick={ this.onClose }
/>,
<Button
disabled={ !canTransact }
icon={ <SendIcon /> }
key='request'
label={
<FormattedMessage
id='faucet.buttons.request'
defaultMessage='request'
/>
}
onClick={ this.onExecute }
/>
];
}
renderSummaryDone () {
const { error, responseText, responseTxHash } = this.store;
return (
<div>
<FormattedMessage
id='faucet.summary.done'
defaultMessage='Your Kovan ETH has been requested from the faucet which responded with -'
/>
{
error
? (
<p>{ error }</p>
)
: (
<p>
<span>{ responseText }&nbsp;</span>
<a href={ txLink(responseTxHash, false, '42') } target='_blank'>
<ShortenedHash data={ responseTxHash } />
</a>
</p>
)
}
</div>
);
}
renderSummaryRequest () {
return (
<FormattedMessage
id='faucet.summary.info'
defaultMessage='To request a deposit of Kovan ETH to this address, you need to ensure that the address is sms-verified on the mainnet. Once executed the faucet will deposit Kovan ETH into the current account.'
/>
);
}
onClose = () => {
this.props.onClose();
}
onExecute = () => {
return this.store.makeItRain();
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './faucet';

View File

@ -0,0 +1,126 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, computed, observable, transaction } from 'mobx';
import apiutil from '~/api/util';
const ENDPOINT = 'http://faucet.kovan.network/api/';
export default class Store {
@observable addressReceive = null;
@observable addressVerified = null;
@observable error = null;
@observable responseText = null;
@observable responseTxHash = null;
@observable isBusy = false;
@observable isCompleted = false;
@observable isDestination = false;
@observable isDone = false;
constructor (netVersion, address) {
transaction(() => {
this.setDestination(netVersion === '42');
this.setAddressReceive(address);
this.setAddressVerified(address);
});
}
@computed get canTransact () {
return !this.isBusy && this.addressReceiveValid && this.addressVerifiedValid;
}
@computed get addressReceiveValid () {
return apiutil.isAddressValid(this.addressReceive);
}
@computed get addressVerifiedValid () {
return apiutil.isAddressValid(this.addressVerified);
}
@action setAddressReceive = (address) => {
this.addressReceive = address;
}
@action setAddressVerified = (address) => {
this.addressVerified = address;
}
@action setBusy = (isBusy) => {
this.isBusy = isBusy;
}
@action setCompleted = (isCompleted) => {
transaction(() => {
this.setBusy(false);
this.isCompleted = isCompleted;
});
}
@action setDestination = (isDestination) => {
this.isDestination = isDestination;
}
@action setError = (error) => {
if (error.indexOf('not certified') !== -1) {
this.error = `${error}. Please ensure that this account is sms certified on the mainnet.`;
} else {
this.error = error;
}
}
@action setResponse = (response) => {
this.responseText = response.result;
this.responseTxHash = response.tx;
}
makeItRain = () => {
this.setBusy(true);
const options = {
method: 'GET',
mode: 'cors'
};
const url = `${ENDPOINT}${this.addressVerified}`;
return fetch(url, options)
.then((response) => {
if (!response.ok) {
return null;
}
return response.json();
})
.catch(() => {
return null;
})
.then((response) => {
transaction(() => {
if (!response || response.error) {
this.setError(
response
? response.error
: 'Unable to complete request to the faucet, the server may be unavailable. Please try again later.'
);
} else {
this.setResponse(response);
}
this.setCompleted(true);
});
});
}
}

View File

@ -401,6 +401,7 @@ class WalletSettings extends Component {
const cancelBtn = ( const cancelBtn = (
<Button <Button
icon={ <CancelIcon /> } icon={ <CancelIcon /> }
key='cancelBtn'
label={ label={
<FormattedMessage <FormattedMessage
id='walletSettings.buttons.cancel' id='walletSettings.buttons.cancel'
@ -414,6 +415,7 @@ class WalletSettings extends Component {
const closeBtn = ( const closeBtn = (
<Button <Button
icon={ <CancelIcon /> } icon={ <CancelIcon /> }
key='closeBtn'
label={ label={
<FormattedMessage <FormattedMessage
id='walletSettings.buttons.close' id='walletSettings.buttons.close'
@ -427,6 +429,7 @@ class WalletSettings extends Component {
const sendingBtn = ( const sendingBtn = (
<Button <Button
icon={ <DoneIcon /> } icon={ <DoneIcon /> }
key='sendingBtn'
label={ label={
<FormattedMessage <FormattedMessage
id='walletSettings.buttons.sending' id='walletSettings.buttons.sending'
@ -440,6 +443,7 @@ class WalletSettings extends Component {
const nextBtn = ( const nextBtn = (
<Button <Button
icon={ <NextIcon /> } icon={ <NextIcon /> }
key='nextBtn'
label={ label={
<FormattedMessage <FormattedMessage
id='walletSettings.buttons.next' id='walletSettings.buttons.next'
@ -454,6 +458,7 @@ class WalletSettings extends Component {
const sendBtn = ( const sendBtn = (
<Button <Button
icon={ <NextIcon /> } icon={ <NextIcon /> }
key='sendBtn'
label={ label={
<FormattedMessage <FormattedMessage
id='walletSettings.buttons.send' id='walletSettings.buttons.send'

View File

@ -24,6 +24,7 @@ export DeleteAccount from './DeleteAccount';
export DeployContract from './DeployContract'; export DeployContract from './DeployContract';
export EditMeta from './EditMeta'; export EditMeta from './EditMeta';
export ExecuteContract from './ExecuteContract'; export ExecuteContract from './ExecuteContract';
export Faucet from './Faucet';
export FirstRun from './FirstRun'; export FirstRun from './FirstRun';
export LoadContract from './LoadContract'; export LoadContract from './LoadContract';
export PasswordManager from './PasswordManager'; export PasswordManager from './PasswordManager';

View File

@ -49,7 +49,7 @@ export default class Personal {
.filter((address) => { .filter((address) => {
const account = accountsInfo[address]; const account = accountsInfo[address];
return !account.uuid && account.meta.deleted; return !account.uuid && account.meta && account.meta.deleted;
}) })
.map((address) => this._api.parity.removeAddress(address)) .map((address) => this._api.parity.removeAddress(address))
); );

View File

@ -26,7 +26,6 @@ import WalletsUtils from '~/util/wallets';
import { wallet as WalletAbi } from '~/contracts/abi'; import { wallet as WalletAbi } from '~/contracts/abi';
export function personalAccountsInfo (accountsInfo) { export function personalAccountsInfo (accountsInfo) {
const addresses = [];
const accounts = {}; const accounts = {};
const contacts = {}; const contacts = {};
const contracts = {}; const contracts = {};
@ -35,10 +34,9 @@ export function personalAccountsInfo (accountsInfo) {
Object.keys(accountsInfo || {}) Object.keys(accountsInfo || {})
.map((address) => Object.assign({}, accountsInfo[address], { address })) .map((address) => Object.assign({}, accountsInfo[address], { address }))
.filter((account) => account.uuid || !account.meta.deleted) .filter((account) => account.meta && (account.uuid || !account.meta.deleted))
.forEach((account) => { .forEach((account) => {
if (account.uuid) { if (account.uuid) {
addresses.push(account.address);
accounts[account.address] = account; accounts[account.address] = account;
} else if (account.meta.wallet) { } else if (account.meta.wallet) {
account.wallet = true; account.wallet = true;
@ -87,18 +85,45 @@ export function personalAccountsInfo (accountsInfo) {
return []; return [];
}) })
.then((_wallets) => { .then((_wallets) => {
_wallets.forEach((wallet) => { // We want to separate owned wallets and other wallets
// However, wallets can be owned by wallets, that can
// be owned by an account...
let otherWallets = [].concat(_wallets);
let prevLength;
let nextLength;
// If no more other wallets, or if the size decreased, continue...
do {
prevLength = otherWallets.length;
otherWallets = otherWallets
.map((wallet) => {
const addresses = Object.keys(accounts);
const owners = wallet.owners.map((o) => o.address); const owners = wallet.owners.map((o) => o.address);
// Owners ∩ Addresses not null : Wallet is owned // Owners ∩ Addresses not null : Wallet is owned
// by one of the accounts // by one of the accounts
if (intersection(owners, addresses).length > 0) { if (intersection(owners, addresses).length > 0) {
accounts[wallet.address] = wallet; accounts[wallet.address] = wallet;
} else { return false;
contacts[wallet.address] = wallet;
} }
return wallet;
})
.filter((wallet) => wallet);
nextLength = otherWallets.length;
} while (nextLength < prevLength);
// And other wallets to contacts...
otherWallets.forEach((wallet) => {
contacts[wallet.address] = wallet;
}); });
// Cache the _real_ accounts for
// WalletsUtils (used for sending transactions)
WalletsUtils.cacheAccounts(accounts);
dispatch(_personalAccountsInfo({ dispatch(_personalAccountsInfo({
accountsInfo, accountsInfo,
accounts, accounts,

View File

@ -431,22 +431,7 @@ function parseLogs (logs) {
return; return;
} }
const { wallet } = getState(); const WalletSignatures = WalletsUtils.getWalletSignatures();
const { contract } = wallet;
const walletInstance = contract.instance;
const signatures = {
OwnerChanged: toHex(walletInstance.OwnerChanged.signature),
OwnerAdded: toHex(walletInstance.OwnerAdded.signature),
OwnerRemoved: toHex(walletInstance.OwnerRemoved.signature),
RequirementChanged: toHex(walletInstance.RequirementChanged.signature),
Confirmation: toHex(walletInstance.Confirmation.signature),
Revoke: toHex(walletInstance.Revoke.signature),
Deposit: toHex(walletInstance.Deposit.signature),
SingleTransact: toHex(walletInstance.SingleTransact.signature),
MultiTransact: toHex(walletInstance.MultiTransact.signature),
ConfirmationNeeded: toHex(walletInstance.ConfirmationNeeded.signature)
};
const updates = {}; const updates = {};
@ -459,25 +444,25 @@ function parseLogs (logs) {
}; };
switch (eventSignature) { switch (eventSignature) {
case signatures.OwnerChanged: case WalletSignatures.OwnerChanged:
case signatures.OwnerAdded: case WalletSignatures.OwnerAdded:
case signatures.OwnerRemoved: case WalletSignatures.OwnerRemoved:
updates[address] = { updates[address] = {
...prev, ...prev,
[ UPDATE_OWNERS ]: true [ UPDATE_OWNERS ]: true
}; };
return; return;
case signatures.RequirementChanged: case WalletSignatures.RequirementChanged:
updates[address] = { updates[address] = {
...prev, ...prev,
[ UPDATE_REQUIRE ]: true [ UPDATE_REQUIRE ]: true
}; };
return; return;
case signatures.ConfirmationNeeded: case WalletSignatures.ConfirmationNeeded:
case signatures.Confirmation: case WalletSignatures.Confirmation:
case signatures.Revoke: case WalletSignatures.Revoke:
const operation = bytesToHex(log.params.operation.value); const operation = bytesToHex(log.params.operation.value);
updates[address] = { updates[address] = {
@ -489,9 +474,11 @@ function parseLogs (logs) {
return; return;
case signatures.Deposit: case WalletSignatures.Deposit:
case signatures.SingleTransact: case WalletSignatures.SingleTransact:
case signatures.MultiTransact: case WalletSignatures.MultiTransact:
case WalletSignatures.Old.SingleTransact:
case WalletSignatures.Old.MultiTransact:
updates[address] = { updates[address] = {
...prev, ...prev,
[ UPDATE_TRANSACTIONS ]: true [ UPDATE_TRANSACTIONS ]: true

View File

@ -31,6 +31,7 @@ export DashboardIcon from 'material-ui/svg-icons/action/dashboard';
export DeleteIcon from 'material-ui/svg-icons/action/delete'; export DeleteIcon from 'material-ui/svg-icons/action/delete';
export DevelopIcon from 'material-ui/svg-icons/action/description'; export DevelopIcon from 'material-ui/svg-icons/action/description';
export DoneIcon from 'material-ui/svg-icons/action/done-all'; export DoneIcon from 'material-ui/svg-icons/action/done-all';
export DialIcon from 'material-ui/svg-icons/communication/dialpad';
export EditIcon from 'material-ui/svg-icons/content/create'; export EditIcon from 'material-ui/svg-icons/content/create';
export ErrorIcon from 'material-ui/svg-icons/alert/error'; export ErrorIcon from 'material-ui/svg-icons/alert/error';
export FileUploadIcon from 'material-ui/svg-icons/file/file-upload'; export FileUploadIcon from 'material-ui/svg-icons/file/file-upload';

View File

@ -120,6 +120,18 @@ class MethodDecoding extends Component {
<span className={ styles.highlight }> <span className={ styles.highlight }>
{ gas.toFormat(0) } gas ({ gasPrice.div(1000000).toFormat(0) }M/<small>ETH</small>) { gas.toFormat(0) } gas ({ gasPrice.div(1000000).toFormat(0) }M/<small>ETH</small>)
</span> </span>
{
transaction.gasUsed
? (
<span>
<span>used</span>
<span className={ styles.highlight }>
{ transaction.gasUsed.toFormat(0) } gas
</span>
</span>
)
: null
}
<span> for a total transaction value of </span> <span> for a total transaction value of </span>
<span className={ styles.highlight }>{ this.renderEtherValue(gasValue) }</span> <span className={ styles.highlight }>{ this.renderEtherValue(gasValue) }</span>
{ this.renderMinBlock() } { this.renderMinBlock() }

View File

@ -22,13 +22,13 @@ import styles from './modalBox.css';
export default class ModalBox extends Component { export default class ModalBox extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node,
icon: PropTypes.node.isRequired, icon: PropTypes.node.isRequired,
summary: nodeOrStringProptype() summary: nodeOrStringProptype()
} }
render () { render () {
const { children, icon } = this.props; const { icon } = this.props;
return ( return (
<div className={ styles.body }> <div className={ styles.body }>
@ -37,11 +37,23 @@ export default class ModalBox extends Component {
</div> </div>
<div className={ styles.content }> <div className={ styles.content }>
{ this.renderSummary() } { this.renderSummary() }
{ this.renderBody() }
</div>
</div>
);
}
renderBody () {
const { children } = this.props;
if (!children) {
return null;
}
return (
<div className={ styles.body }> <div className={ styles.body }>
{ children } { children }
</div> </div>
</div>
</div>
); );
} }

View File

@ -17,7 +17,6 @@
$transition: all 0.25s; $transition: all 0.25s;
$widthNormal: 33.33%; $widthNormal: 33.33%;
$widthShrunk: 29%;
$widthExpanded: 42%; $widthExpanded: 42%;
.section { .section {
@ -39,18 +38,19 @@ $widthExpanded: 42%;
display: flex; display: flex;
justify-content: center; justify-content: center;
/* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for */ /* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for
/* case where <> 3 columns are required should the need arrise from a UI pov. */ /* case where <> 3 columns are required should the need arrise from a UI pov. */
.item { .item {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex: 0 1 $widthNormal; flex: 0 1 $widthNormal;
max-width: $widthNormal;
opacity: 0.85; opacity: 0.85;
padding: 0.25em; padding: 0.25em;
/* https://www.binarymoon.co.uk/2014/02/fixing-css-transitions-in-google-chrome/ */ /* https://www.binarymoon.co.uk/2014/02/fixing-css-transitions-in-google-chrome/ */
transform: translateZ(0); transform: translateZ(0);
transition: $transition; transition: $transition;
width: 0;
&:hover { &:hover {
opacity: 1; opacity: 1;
@ -58,21 +58,12 @@ $widthExpanded: 42%;
} }
} }
&:hover { .item.stretchOn:hover {
.item {
&.stretchOn {
flex: 0 1 $widthShrunk;
max-width: $widthShrunk;
&:hover {
flex: 0 0 $widthExpanded; flex: 0 0 $widthExpanded;
max-width: $widthExpanded; max-width: $widthExpanded;
} }
} }
} }
}
}
}
.section + .section { .section + .section {
margin-top: 1em; margin-top: 1em;

View File

@ -51,6 +51,7 @@ export default class Store {
return bnB.comparedTo(bnA); return bnB.comparedTo(bnA);
}); });
this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0)); this._pendingHashes = this.sortedHashes.filter((hash) => this.transactions[hash].blockNumber.eq(0));
}); });
} }
@ -85,26 +86,53 @@ export default class Store {
this._subscriptionId = 0; this._subscriptionId = 0;
} }
loadTransactions (_txhashes) { loadTransactions (_txhashes = []) {
const txhashes = _txhashes.filter((hash) => !this.transactions[hash] || this._pendingHashes.includes(hash)); const promises = _txhashes
.filter((txhash) => !this.transactions[txhash] || this._pendingHashes.includes(txhash))
.map((txhash) => {
return Promise
.all([
this._api.eth.getTransactionByHash(txhash),
this._api.eth.getTransactionReceipt(txhash)
])
.then(([
transaction = {},
transactionReceipt = {}
]) => {
return {
...transactionReceipt,
...transaction
};
});
});
if (!txhashes || !txhashes.length) { if (!promises.length) {
return; return;
} }
Promise Promise
.all(txhashes.map((txhash) => this._api.eth.getTransactionByHash(txhash))) .all(promises)
.then((_transactions) => { .then((_transactions) => {
const transactions = _transactions.filter((tx) => tx); const blockNumbers = [];
const transactions = _transactions
.filter((tx) => tx && tx.hash)
.reduce((txs, tx) => {
txs[tx.hash] = tx;
this.addTransactions( if (tx.blockNumber && tx.blockNumber.gt(0)) {
transactions.reduce((transactions, tx, index) => { blockNumbers.push(tx.blockNumber.toNumber());
transactions[txhashes[index]] = tx; }
return transactions;
}, {})
);
this.loadBlocks(transactions.map((tx) => tx.blockNumber ? tx.blockNumber.toNumber() : 0)); return txs;
}, {});
// No need to add transactions if there are none
if (Object.keys(transactions).length === 0) {
return false;
}
this.addTransactions(transactions);
this.loadBlocks(blockNumbers);
}) })
.catch((error) => { .catch((error) => {
console.warn('loadTransactions', error); console.warn('loadTransactions', error);

View File

@ -27,7 +27,7 @@ import styles from './txList.css';
class TxList extends Component { class TxList extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
@ -36,7 +36,7 @@ class TxList extends Component {
PropTypes.object PropTypes.object
]).isRequired, ]).isRequired,
netVersion: PropTypes.string.isRequired netVersion: PropTypes.string.isRequired
} };
store = new Store(this.context.api); store = new Store(this.context.api);

View File

@ -61,17 +61,6 @@ export function estimateGas (_func, _options, _values = []) {
const { func, options, values } = callArgs; const { func, options, values } = callArgs;
return func._estimateGas(options, values); return func._estimateGas(options, values);
})
.then((gas) => {
return WalletsUtils
.isWallet(_func.contract.api, _options.from)
.then((isWallet) => {
if (isWallet) {
return gas.mul(1.5);
}
return gas;
});
}); });
} }
@ -84,6 +73,114 @@ export function postTransaction (_func, _options, _values = []) {
}); });
} }
export function deploy (contract, _options, values, metadata = {}, statecb = () => {}) {
const options = { ..._options };
const { api } = contract;
const address = options.from;
return WalletsUtils
.isWallet(api, address)
.then((isWallet) => {
if (!isWallet) {
return contract.deploy(options, values, statecb);
}
statecb(null, { state: 'estimateGas' });
return deployEstimateGas(contract, options, values)
.then(([gasEst, gas]) => {
options.gas = gas.toFixed(0);
statecb(null, { state: 'postTransaction', gas });
return WalletsUtils.getDeployArgs(contract, options, values);
})
.then((callArgs) => {
const { func, options, values } = callArgs;
return func._postTransaction(options, values)
.then((requestId) => {
statecb(null, { state: 'checkRequest', requestId });
return contract._pollCheckRequest(requestId);
})
.then((txhash) => {
statecb(null, { state: 'getTransactionReceipt', txhash });
return contract._pollTransactionReceipt(txhash, options.gas);
})
.then((receipt) => {
if (receipt.gasUsed.eq(options.gas)) {
throw new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`);
}
const logs = WalletsUtils.parseLogs(api, receipt.logs || []);
const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded');
const transactionLog = logs.find((log) => log.event === 'SingleTransact');
if (!confirmationLog && !transactionLog) {
throw new Error('Something went wrong in the Wallet Contract (no logs have been emitted)...');
}
// Confirmations are needed from the other owners
if (confirmationLog) {
const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value);
// Add the contract to pending contracts
WalletsUtils.addPendingContract(address, operationHash, metadata);
statecb(null, { state: 'confirmationNeeded' });
return;
}
// Set the contract address in the receip
receipt.contractAddress = transactionLog.params.created.value;
const contractAddress = receipt.contractAddress;
statecb(null, { state: 'hasReceipt', receipt });
contract._receipt = receipt;
contract._address = contractAddress;
statecb(null, { state: 'getCode' });
return api.eth.getCode(contractAddress)
.then((code) => {
if (code === '0x') {
throw new Error('Contract not deployed, getCode returned 0x');
}
statecb(null, { state: 'completed' });
return contractAddress;
});
});
});
});
}
export function deployEstimateGas (contract, _options, values) {
const options = { ..._options };
const { api } = contract;
const address = options.from;
return WalletsUtils
.isWallet(api, address)
.then((isWallet) => {
if (!isWallet) {
return contract.deployEstimateGas(options, values);
}
return WalletsUtils
.getDeployArgs(contract, options, values)
.then((callArgs) => {
const { func, options, values } = callArgs;
return func.estimateGas(options, values);
})
.then((gasEst) => {
return [gasEst, gasEst.mul(1.05)];
});
});
}
export function patchApi (api) { export function patchApi (api) {
api.patch = { api.patch = {
...api.patch, ...api.patch,

View File

@ -16,59 +16,154 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { intersection, range, uniq } from 'lodash'; import { intersection, range, uniq } from 'lodash';
import store from 'store';
import Abi from '~/abi';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import { bytesToHex, toHex } from '~/api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation'; import { validateAddress } from '~/util/validation';
import WalletAbi from '~/contracts/abi/wallet.json'; import WalletAbi from '~/contracts/abi/wallet.json';
import OldWalletAbi from '~/contracts/abi/old-wallet.json';
const LS_PENDING_CONTRACTS_KEY = '_parity::wallets::pendingContracts';
const _cachedWalletLookup = {}; const _cachedWalletLookup = {};
let _cachedAccounts = {};
const walletAbi = new Abi(WalletAbi);
const oldWalletAbi = new Abi(OldWalletAbi);
const walletEvents = walletAbi.events.reduce((events, event) => {
events[event.name] = event;
return events;
}, {});
const oldWalletEvents = oldWalletAbi.events.reduce((events, event) => {
events[event.name] = event;
return events;
}, {});
const WalletSignatures = {
OwnerChanged: toHex(walletEvents.OwnerChanged.signature),
OwnerAdded: toHex(walletEvents.OwnerAdded.signature),
OwnerRemoved: toHex(walletEvents.OwnerRemoved.signature),
RequirementChanged: toHex(walletEvents.RequirementChanged.signature),
Confirmation: toHex(walletEvents.Confirmation.signature),
Revoke: toHex(walletEvents.Revoke.signature),
Deposit: toHex(walletEvents.Deposit.signature),
SingleTransact: toHex(walletEvents.SingleTransact.signature),
MultiTransact: toHex(walletEvents.MultiTransact.signature),
ConfirmationNeeded: toHex(walletEvents.ConfirmationNeeded.signature),
Old: {
SingleTransact: toHex(oldWalletEvents.SingleTransact.signature),
MultiTransact: toHex(oldWalletEvents.MultiTransact.signature)
}
};
export default class WalletsUtils { export default class WalletsUtils {
static getWalletSignatures () {
return WalletSignatures;
}
static getPendingContracts () {
return store.get(LS_PENDING_CONTRACTS_KEY) || {};
}
static setPendingContracts (contracts = {}) {
return store.set(LS_PENDING_CONTRACTS_KEY, contracts);
}
static removePendingContract (operationHash) {
const nextContracts = WalletsUtils.getPendingContracts();
delete nextContracts[operationHash];
WalletsUtils.setPendingContracts(nextContracts);
}
static addPendingContract (address, operationHash, metadata) {
const nextContracts = {
...WalletsUtils.getPendingContracts(),
[ operationHash ]: {
address,
metadata,
operationHash
}
};
WalletsUtils.setPendingContracts(nextContracts);
}
static cacheAccounts (accounts) {
_cachedAccounts = accounts;
}
static getCallArgs (api, options, values = []) { static getCallArgs (api, options, values = []) {
const walletContract = new Contract(api, WalletAbi); const walletContract = new Contract(api, WalletAbi);
const walletAddress = options.from;
const promises = [ return WalletsUtils
api.parity.accountsInfo(), .fetchOwners(walletContract.at(walletAddress))
WalletsUtils.fetchOwners(walletContract.at(options.from)) .then((owners) => {
]; const addresses = Object.keys(_cachedAccounts);
const ownerAddress = intersection(addresses, owners).pop();
return Promise if (!ownerAddress) {
.all(promises)
.then(([ accounts, owners ]) => {
const addresses = Object.keys(accounts);
const owner = intersection(addresses, owners).pop();
if (!owner) {
return false; return false;
} }
return owner; const account = _cachedAccounts[ownerAddress];
}) const _options = { ...options };
.then((owner) => { const { to, value = new BigNumber(0), data } = _options;
if (!owner) {
return false;
}
const _options = Object.assign({}, options);
const { from, to, value = new BigNumber(0), data } = options;
delete _options.data; delete _options.data;
const nextValues = [ to, value, data ]; const nextValues = [ to, value, data ];
const nextOptions = { const nextOptions = {
..._options, ..._options,
from: owner, from: ownerAddress,
to: from, to: walletAddress,
value: new BigNumber(0) value: new BigNumber(0)
}; };
const execFunc = walletContract.instance.execute; const execFunc = walletContract.instance.execute;
const callArgs = { func: execFunc, options: nextOptions, values: nextValues };
return { func: execFunc, options: nextOptions, values: nextValues }; if (!account.wallet) {
return callArgs;
}
const nextData = walletContract.getCallData(execFunc, nextOptions, nextValues);
return WalletsUtils.getCallArgs(api, { ...nextOptions, data: nextData }, nextValues);
}); });
} }
static getDeployArgs (contract, options, values) {
const { api } = contract;
const func = contract.constructors[0];
options.data = contract.getCallData(func, options, values);
options.to = '0x';
return WalletsUtils
.getCallArgs(api, options, values)
.then((callArgs) => {
if (!callArgs) {
console.error('no call args', callArgs);
throw new Error('you do not own this wallet');
}
return callArgs;
});
}
static parseLogs (api, logs = []) {
const walletContract = new Contract(api, WalletAbi);
return walletContract.parseEventLogs(logs);
}
/** /**
* Check whether the given address could be * Check whether the given address could be
* a Wallet. The result is cached in order not * a Wallet. The result is cached in order not
@ -199,16 +294,18 @@ export default class WalletsUtils {
} }
static fetchTransactions (walletContract) { static fetchTransactions (walletContract) {
const walletInstance = walletContract.instance; const { api } = walletContract;
const signatures = { const pendingContracts = WalletsUtils.getPendingContracts();
single: toHex(walletInstance.SingleTransact.signature),
multi: toHex(walletInstance.MultiTransact.signature),
deposit: toHex(walletInstance.Deposit.signature)
};
return walletContract return walletContract
.getAllLogs({ .getAllLogs({
topics: [ [ signatures.single, signatures.multi, signatures.deposit ] ] topics: [ [
WalletSignatures.SingleTransact,
WalletSignatures.MultiTransact,
WalletSignatures.Deposit,
WalletSignatures.Old.SingleTransact,
WalletSignatures.Old.MultiTransact
] ]
}) })
.then((logs) => { .then((logs) => {
return logs.sort((logA, logB) => { return logs.sort((logA, logB) => {
@ -226,11 +323,11 @@ export default class WalletsUtils {
const signature = toHex(log.topics[0]); const signature = toHex(log.topics[0]);
const value = log.params.value.value; const value = log.params.value.value;
const from = signature === signatures.deposit const from = signature === WalletSignatures.Deposit
? log.params['_from'].value ? log.params['_from'].value
: walletContract.address; : walletContract.address;
const to = signature === signatures.deposit const to = signature === WalletSignatures.Deposit
? walletContract.address ? walletContract.address
: log.params.to.value; : log.params.to.value;
@ -240,8 +337,53 @@ export default class WalletsUtils {
from, to, value from, to, value
}; };
if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) {
transaction.creates = log.params.created.value;
delete transaction.to;
}
if (log.params.operation) { if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value); const operation = bytesToHex(log.params.operation.value);
// Add the pending contract to the contracts
if (pendingContracts[operation]) {
const { metadata } = pendingContracts[operation];
const contractName = metadata.name;
metadata.blockNumber = log.blockNumber;
// The contract creation might not be in the same log,
// but must be in the same transaction (eg. Contract creation
// from Wallet within a Wallet)
api.eth
.getTransactionReceipt(log.transactionHash)
.then((transactionReceipt) => {
const transactionLogs = WalletsUtils.parseLogs(api, transactionReceipt.logs);
const creationLog = transactionLogs.find((log) => {
return log.params.created && !/^(0x)?0*$/.test(log.params.created.value);
});
if (!creationLog) {
return false;
}
const contractAddress = creationLog.params.created.value;
return Promise
.all([
api.parity.setAccountName(contractAddress, contractName),
api.parity.setAccountMeta(contractAddress, metadata)
])
.then(() => {
WalletsUtils.removePendingContract(operation);
});
})
.catch((error) => {
console.error('adding wallet contract', error);
});
}
transaction.operation = operation;
} }
if (log.params.data) { if (log.params.data) {

View File

@ -22,11 +22,11 @@ import { bindActionCreators } from 'redux';
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png'; import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
import HardwareStore from '~/mobx/hardwareStore'; import HardwareStore from '~/mobx/hardwareStore';
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals'; import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions'; import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { Actionbar, Button, Page } from '~/ui'; import { Actionbar, Button, Page } from '~/ui';
import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons'; import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
import DeleteAddress from '../Address/Delete'; import DeleteAddress from '../Address/Delete';
@ -48,6 +48,8 @@ class Account extends Component {
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object, balances: PropTypes.object,
certifications: PropTypes.object,
netVersion: PropTypes.string.isRequired,
params: PropTypes.object params: PropTypes.object
} }
@ -97,6 +99,7 @@ class Account extends Component {
<div> <div>
{ this.renderDeleteDialog(account) } { this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) } { this.renderEditDialog(account) }
{ this.renderFaucetDialog() }
{ this.renderFundDialog() } { this.renderFundDialog() }
{ this.renderPasswordDialog(account) } { this.renderPasswordDialog(account) }
{ this.renderTransferDialog(account, balance) } { this.renderTransferDialog(account, balance) }
@ -117,8 +120,35 @@ class Account extends Component {
); );
} }
isKovan = (netVersion) => {
return netVersion === '42';
}
isMainnet = (netVersion) => {
return netVersion === '1';
}
isFaucettable = (netVersion, certifications, address) => {
return this.isKovan(netVersion) || (
this.isMainnet(netVersion) &&
this.isSmsCertified(certifications, address)
);
}
isSmsCertified = (_certifications, address) => {
const certifications = _certifications && _certifications[address]
? _certifications[address].filter((cert) => cert.name.indexOf('smsverification') === 0)
: [];
return certifications.length !== 0;
}
renderActionbar (account, balance) { renderActionbar (account, balance) {
const { certifications, netVersion } = this.props;
const { address } = this.props.params;
const showTransferButton = !!(balance && balance.tokens); const showTransferButton = !!(balance && balance.tokens);
const isVerifiable = this.isMainnet(netVersion);
const isFaucettable = this.isFaucettable(netVersion, certifications, address);
const buttons = [ const buttons = [
<Button <Button
@ -149,9 +179,11 @@ class Account extends Component {
} }
onClick={ this.store.toggleFundDialog } onClick={ this.store.toggleFundDialog }
/>, />,
isVerifiable
? (
<Button <Button
icon={ <VerifyIcon /> } icon={ <VerifyIcon /> }
key='sms-verification' key='verification'
label={ label={
<FormattedMessage <FormattedMessage
id='account.button.verify' id='account.button.verify'
@ -159,7 +191,24 @@ class Account extends Component {
/> />
} }
onClick={ this.store.toggleVerificationDialog } onClick={ this.store.toggleVerificationDialog }
/>, />
)
: null,
isFaucettable
? (
<Button
icon={ <DialIcon /> }
key='faucet'
label={
<FormattedMessage
id='account.button.faucet'
defaultMessage='Kovan ETH'
/>
}
onClick={ this.store.toggleFaucetDialog }
/>
)
: null,
<Button <Button
icon={ <EditIcon /> } icon={ <EditIcon /> }
key='editmeta' key='editmeta'
@ -253,6 +302,24 @@ class Account extends Component {
); );
} }
renderFaucetDialog () {
const { netVersion } = this.props;
if (!this.store.isFaucetVisible) {
return null;
}
const { address } = this.props.params;
return (
<Faucet
address={ address }
netVersion={ netVersion }
onClose={ this.store.toggleFaucetDialog }
/>
);
}
renderFundDialog () { renderFundDialog () {
if (!this.store.isFundVisible) { if (!this.store.isFundVisible) {
return null; return null;
@ -317,10 +384,14 @@ class Account extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts } = state.personal; const { accounts } = state.personal;
const { balances } = state.balances; const { balances } = state.balances;
const certifications = state.certifications;
const { netVersion } = state.nodeStatus;
return { return {
accounts, accounts,
balances balances,
certifications,
netVersion
}; };
} }

View File

@ -80,57 +80,16 @@ describe('views/Account', () => {
describe('sub-renderers', () => { describe('sub-renderers', () => {
describe('renderActionBar', () => { describe('renderActionBar', () => {
let bar; let bar;
let barShallow;
beforeEach(() => { beforeEach(() => {
render(); render();
bar = instance.renderActionbar({ tokens: {} }); bar = instance.renderActionbar({ tokens: {} });
barShallow = shallow(bar);
}); });
it('renders the bar', () => { it('renders the bar', () => {
expect(bar.type).to.match(/Actionbar/); expect(bar.type).to.match(/Actionbar/);
}); });
// TODO: Finding by index is not optimal, however couldn't find a better method atm
// since we cannot find by key (prop not visible in shallow debug())
describe('clicks', () => {
it('toggles transfer on click', () => {
barShallow.find('Button').at(0).simulate('click');
expect(store.isTransferVisible).to.be.true;
});
it('toggles fund on click', () => {
barShallow.find('Button').at(1).simulate('click');
expect(store.isFundVisible).to.be.true;
});
it('toggles fund on click', () => {
barShallow.find('Button').at(1).simulate('click');
expect(store.isFundVisible).to.be.true;
});
it('toggles verify on click', () => {
barShallow.find('Button').at(2).simulate('click');
expect(store.isVerificationVisible).to.be.true;
});
it('toggles edit on click', () => {
barShallow.find('Button').at(3).simulate('click');
expect(store.isEditVisible).to.be.true;
});
it('toggles password on click', () => {
barShallow.find('Button').at(4).simulate('click');
expect(store.isPasswordVisible).to.be.true;
});
it('toggles delete on click', () => {
barShallow.find('Button').at(5).simulate('click');
expect(store.isDeleteVisible).to.be.true;
});
});
}); });
describe('renderDeleteDialog', () => { describe('renderDeleteDialog', () => {

View File

@ -19,6 +19,7 @@ import { action, observable } from 'mobx';
export default class Store { export default class Store {
@observable isDeleteVisible = false; @observable isDeleteVisible = false;
@observable isEditVisible = false; @observable isEditVisible = false;
@observable isFaucetVisible = false;
@observable isFundVisible = false; @observable isFundVisible = false;
@observable isPasswordVisible = false; @observable isPasswordVisible = false;
@observable isTransferVisible = false; @observable isTransferVisible = false;
@ -32,6 +33,10 @@ export default class Store {
this.isEditVisible = !this.isEditVisible; this.isEditVisible = !this.isEditVisible;
} }
@action toggleFaucetDialog = () => {
this.isFaucetVisible = !this.isFaucetVisible;
}
@action toggleFundDialog = () => { @action toggleFundDialog = () => {
this.isFundVisible = !this.isFundVisible; this.isFundVisible = !this.isFundVisible;
} }

View File

@ -31,6 +31,7 @@ describe('views/Account/Store', () => {
it('sets all modal visibility to false', () => { it('sets all modal visibility to false', () => {
expect(store.isDeleteVisible).to.be.false; expect(store.isDeleteVisible).to.be.false;
expect(store.isEditVisible).to.be.false; expect(store.isEditVisible).to.be.false;
expect(store.isFaucetVisible).to.be.false;
expect(store.isFundVisible).to.be.false; expect(store.isFundVisible).to.be.false;
expect(store.isPasswordVisible).to.be.false; expect(store.isPasswordVisible).to.be.false;
expect(store.isTransferVisible).to.be.false; expect(store.isTransferVisible).to.be.false;
@ -53,6 +54,13 @@ describe('views/Account/Store', () => {
}); });
}); });
describe('toggleFaucetDialog', () => {
it('toggles the visibility', () => {
store.toggleFaucetDialog();
expect(store.isFaucetVisible).to.be.true;
});
});
describe('toggleFundDialog', () => { describe('toggleFundDialog', () => {
it('toggles the visibility', () => { it('toggles the visibility', () => {
store.toggleFundDialog(); store.toggleFundDialog();

View File

@ -41,8 +41,9 @@ export default class SignerStore {
this.balances = Object.assign({}, this.balances, balances); this.balances = Object.assign({}, this.balances, balances);
} }
@action setLocalHashes = (localHashes) => { @action setLocalHashes = (localHashes = []) => {
if (!isEqual(localHashes, this.localHashes)) { // Use slice to make sure they are both Arrays (MobX uses Objects for Observable Arrays)
if (!isEqual(localHashes.slice(), this.localHashes.slice())) {
this.localHashes = localHashes; this.localHashes = localHashes;
} }
} }

View File

@ -71,7 +71,7 @@ export default class WalletTransactions extends Component {
} }
const txRows = transactions.slice(0, 15).map((transaction, index) => { const txRows = transactions.slice(0, 15).map((transaction, index) => {
const { transactionHash, blockNumber, from, to, value, data } = transaction; const { transactionHash, data } = transaction;
return ( return (
<TxRow <TxRow
@ -79,12 +79,9 @@ export default class WalletTransactions extends Component {
netVersion={ netVersion } netVersion={ netVersion }
key={ `${transactionHash}_${index}` } key={ `${transactionHash}_${index}` }
tx={ { tx={ {
blockNumber,
from,
hash: transactionHash, hash: transactionHash,
input: data && bytesToHex(data) || '', input: data && bytesToHex(data) || '',
to, ...transaction
value
} } } }
/> />
); );