[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,23 +134,23 @@ 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) => { statecb(null, { state: 'getCode' });
setState({ state: 'getCode' }); return this._api.eth.getCode(this._address);
return this._api.eth.getCode(this._address); })
}) .then((code) => {
.then((code) => { if (code === '0x') {
if (code === '0x') { 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;
});
}); });
} }

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,453 +8,454 @@
// 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;
// EVENTS
// this contract can accept a confirmation, in which case contract WalletEvents {
// we record owner and operation (hash) alongside it. // EVENTS
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing. // this contract only has six types of events: it can accept a confirmation, in which case
event OwnerChanged(address oldOwner, address newOwner); // we record owner and operation (hash) alongside it.
event OwnerAdded(address newOwner); event Confirmation(address owner, bytes32 operation);
event OwnerRemoved(address oldOwner); event Revoke(address owner, bytes32 operation);
// the last one is emitted if the required signatures change // some others are in the case of an owner changing.
event RequirementChanged(uint newRequirement); event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// Funds has arrived into the wallet (record how much). // the last one is emitted if the required signatures change
event Deposit(address _from, uint value); event RequirementChanged(uint newRequirement);
// 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); // Funds has arrived into the wallet (record how much).
// 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 Deposit(address _from, uint value);
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
// Confirmation still needed for a transaction. event SingleTransact(address owner, uint value, address to, bytes data, address created);
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); // 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, address created);
// Confirmation still needed for a transaction.
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;
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. function removeOwner(address _owner) external;
function setDailyLimit(uint _newLimit);
function addOwner(address _owner); function changeRequirement(uint _newRequired) external;
function removeOwner(address _owner); function isOwner(address _addr) constant returns (bool);
function changeRequirement(uint _newRequired); function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool);
// Revokes a prior confirmation of the given operation // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function revoke(bytes32 _operation); function setDailyLimit(uint _newLimit) external;
function changeOwner(address _from, address _to); function execute(address _to, uint _value, bytes _data) external returns (bytes32 o_hash);
function confirm(bytes32 _h) returns (bool o_success);
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.
struct PendingState { struct PendingState {
uint yetNeeded; uint yetNeeded;
uint ownersDone; uint ownersDone;
uint index; uint index;
}
// Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction {
address to;
uint value;
bytes data;
}
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// 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
// as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
m_required = _required;
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
var pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
// 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;
}
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
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
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
// 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.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i) {
delete m_txs[m_pendingIndex[i]];
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
} }
// Transaction structure to remember details of transaction lest it need be saved for a later call. delete m_pendingIndex;
struct Transaction { }
address to;
uint value;
bytes data;
}
/****************************** // FIELDS
***** MULTI OWNED SECTION **** address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
******************************/
// MODIFIERS // the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
// simple single-sig function modifier. uint public m_dailyLimit;
modifier onlyowner { uint public m_spentToday;
if (isOwner(msg.sender)) uint public m_lastDay;
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS // list of owners
uint[256] m_owners;
// constructor is given number of sigs required to do protected "onlymanyowners" transactions uint constant c_maxOwners = 250;
// as well as the selection of addresses capable of confirming them. // index on the list of owners to allow reverse lookup
function initMultiowned(address[] _owners, uint _required) { mapping(uint => uint) m_ownerIndex;
m_numOwners = _owners.length + 1; // the ongoing operations.
m_owners[1] = uint(msg.sender); mapping(bytes32 => PendingState) m_pending;
m_ownerIndex[uint(msg.sender)] = 1; bytes32[] m_pendingIndex;
m_required = _required;
for (uint i = 0; i < _owners.length; ++i) // pending transactions we have at present.
{ mapping (bytes32 => Transaction) m_txs;
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
var pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
function isOwner(address _addr) returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
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
// returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
/******************************
********* 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;
for (uint i = 0; i < length; ++i)
delete m_txs[m_pendingIndex[i]];
clearPending();
}
// FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run.
uint m_required;
// pointer used to find a free slot in m_owners
uint m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
} }
contract Wallet is WalletEvents {
contract Wallet is multisig { // WALLET CONSTRUCTOR
// calls the `initWallet` method of the Library in this context
function Wallet(address[] _owners, uint _required, uint _daylimit) {
// Signature of the Wallet Library's init function
bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
address target = _walletLibrary;
// WALLET CONSTRUCTOR // Compute the size of the call data : arrays has 2
// calls the `initWallet` method of the Library in this context // 32bytes for offset and length, plus 32bytes per element ;
function Wallet(address[] _owners, uint _required, uint _daylimit) { // plus 2 32bytes for each uint
// Signature of the Wallet Library's init function uint argarraysize = (2 + _owners.length);
bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)")); uint argsize = (2 + argarraysize) * 32;
address target = _walletLibrary;
// Compute the size of the call data : arrays has 2 assembly {
// 32bytes for offset and length, plus 32bytes per element ; // Add the signature first to memory
// plus 2 32bytes for each uint mstore(0x0, sig)
uint argarraysize = (2 + _owners.length); // Add the call data, which is at the end of the
uint argsize = (2 + argarraysize) * 32; // code
codecopy(0x4, sub(codesize, argsize), argsize)
assembly { // Delegate call to the library
// Add the signature first to memory delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0)
mstore(0x0, sig)
// Add the call data, which is at the end of the
// code
codecopy(0x4, sub(codesize, argsize), argsize)
// Delegate call to the library
delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0)
}
} }
}
// METHODS // METHODS
// gets called when no other function matches // gets called when no other function matches
function() payable { function() payable {
// just being sent some cash? // just being sent some cash?
if (msg.value > 0) if (msg.value > 0)
Deposit(msg.sender, msg.value); Deposit(msg.sender, msg.value);
else if (msg.data.length > 0) else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data); _walletLibrary.delegatecall(msg.data);
} }
// Gets an owner by 0-indexed position (using numOwners as the count) // Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) constant returns (address) { function getOwner(uint ownerIndex) constant returns (address) {
return address(m_owners[ownerIndex + 1]); return address(m_owners[ownerIndex + 1]);
} }
// 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);
} }
// 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 public 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 public m_numOwners; uint public m_numOwners;
uint public m_dailyLimit; uint public m_dailyLimit;
uint public m_spentToday; uint public m_spentToday;
uint public m_lastDay; uint public m_lastDay;
// list of owners // list of owners
uint[256] m_owners; uint[256] m_owners;
} }

View File

@ -8,221 +8,222 @@
// 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 {
// TYPES // TYPES
// struct for the status of a pending operation. // struct for the status of a pending operation.
struct PendingState { struct PendingState {
uint yetNeeded; uint yetNeeded;
uint ownersDone; uint ownersDone;
uint index; uint index;
}
// EVENTS
// this contract only has six types of events: it can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS
// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function multiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
} }
m_required = _required;
}
// EVENTS // Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external {
// this contract only has six types of events: it can accept a confirmation, in which case uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// we record owner and operation (hash) alongside it. // make sure they're an owner
event Confirmation(address owner, bytes32 operation); if (ownerIndex == 0) return;
event Revoke(address owner, bytes32 operation); uint ownerIndexBit = 2**ownerIndex;
// some others are in the case of an owner changing. var pending = m_pending[_operation];
event OwnerChanged(address oldOwner, address newOwner); if (pending.ownersDone & ownerIndexBit > 0) {
event OwnerAdded(address newOwner); pending.yetNeeded++;
event OwnerRemoved(address oldOwner); pending.ownersDone -= ownerIndexBit;
// the last one is emitted if the required signatures change Revoke(msg.sender, _operation);
event RequirementChanged(uint newRequirement);
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
} }
// multi-sig function modifier: the operation must have an intrinsic hash in order }
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations. // Replaces an owner `_from` with another `_to`.
modifier onlymanyowners(bytes32 _operation) { function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external {
if (confirmAndCheck(_operation)) if (isOwner(_to)) return;
_; uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
// 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;
}
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
} }
// determine the bit to set for this owner.
// METHODS uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
// constructor is given number of sigs required to do protected "onlymanyowners" transactions if (pending.ownersDone & ownerIndexBit == 0) {
// as well as the selection of addresses capable of confirming them. Confirmation(msg.sender, _operation);
function multiowned(address[] _owners, uint _required) { // ok - check if count is enough to go ahead.
m_numOwners = _owners.length + 1; if (pending.yetNeeded <= 1) {
m_owners[1] = uint(msg.sender); // enough confirmations: reset and run interior.
m_ownerIndex[uint(msg.sender)] = 1; delete m_pendingIndex[m_pending[_operation].index];
for (uint i = 0; i < _owners.length; ++i) delete m_pending[_operation];
{ return true;
m_owners[2 + i] = uint(_owners[i]); }
m_ownerIndex[uint(_owners[i])] = 2 + i; else
} {
m_required = _required; // not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
} }
}
// Revokes a prior confirmation of the given operation function reorganizeOwners() private {
function revoke(bytes32 _operation) external { uint free = 1;
uint ownerIndex = m_ownerIndex[uint(msg.sender)]; while (free < m_numOwners)
// make sure they're an owner {
if (ownerIndex == 0) return; while (free < m_numOwners && m_owners[free] != 0) free++;
uint ownerIndexBit = 2**ownerIndex; while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
var pending = m_pending[_operation]; if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
if (pending.ownersDone & ownerIndexBit > 0) { {
pending.yetNeeded++; m_owners[free] = m_owners[m_numOwners];
pending.ownersDone -= ownerIndexBit; m_ownerIndex[m_owners[free]] = free;
Revoke(msg.sender, _operation); m_owners[m_numOwners] = 0;
} }
} }
}
// Replaces an owner `_from` with another `_to`. function clearPending() internal {
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { uint length = m_pendingIndex.length;
if (isOwner(_to)) return; for (uint i = 0; i < length; ++i)
uint ownerIndex = m_ownerIndex[uint(_from)]; if (m_pendingIndex[i] != 0)
if (ownerIndex == 0) return; delete m_pending[m_pendingIndex[i]];
delete m_pendingIndex;
}
clearPending(); // FIELDS
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { // the number of owners that must confirm the same operation before it is run.
if (isOwner(_owner)) return; uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
clearPending(); // list of owners
if (m_numOwners >= c_maxOwners) uint[256] m_owners;
reorganizeOwners(); uint constant c_maxOwners = 250;
if (m_numOwners >= c_maxOwners) // index on the list of owners to allow reverse lookup
return; mapping(uint => uint) m_ownerIndex;
m_numOwners++; // the ongoing operations.
m_owners[m_numOwners] = uint(_owner); mapping(bytes32 => PendingState) m_pending;
m_ownerIndex[uint(_owner)] = m_numOwners; bytes32[] m_pendingIndex;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
// 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) returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
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;
}
// FIELDS
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
// list of owners
uint[256] m_owners;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
} }
// inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable) // inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable)
@ -230,79 +231,70 @@ contract multiowned {
// uses is specified in the modifier. // uses is specified in the modifier.
contract daylimit is multiowned { contract daylimit is multiowned {
// MODIFIERS // METHODS
// simple modifier for daily limit. // constructor - stores initial daily limit and records the present day's index.
modifier limitedDaily(uint _value) { function daylimit(uint _limit) {
if (underLimit(_value)) 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;
}
// INTERNAL METHODS
// 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.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
} }
// check to see if there's enough left - if so, subtract and return true.
// METHODS // overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
// constructor - stores initial daily limit and records the present day's index. m_spentToday += _value;
function daylimit(uint _limit) { return true;
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;
} }
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
// INTERNAL METHODS // FIELDS
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and uint public m_dailyLimit;
// returns true. otherwise just returns false. uint public m_spentToday;
function underLimit(uint _value) internal onlyowner returns (bool) { uint public m_lastDay;
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
// FIELDS
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
} }
// interface contract for multisig proxy contracts; see below for docs. // interface contract for multisig proxy contracts; see below for docs.
contract multisig { contract multisig {
// EVENTS // EVENTS
// logged events: // logged events:
// 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:
@ -310,79 +302,102 @@ contract multisig {
// Wallet(w).from(anotherOwner).confirm(h); // Wallet(w).from(anotherOwner).confirm(h);
contract Wallet is multisig, multiowned, daylimit { contract Wallet is multisig, multiowned, daylimit {
// TYPES // TYPES
// Transaction structure to remember details of transaction lest it need be saved for a later call. // Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction { struct Transaction {
address to; address to;
uint value; uint value;
bytes data; bytes data;
}
// METHODS
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function Wallet(address[] _owners, uint _required, uint _daylimit)
multiowned(_owners, _required) daylimit(_daylimit) {
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
}
// 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);
}
} }
}
// METHODS function create(uint _value, bytes _code) internal returns (address o_addr) {
assembly {
// constructor - just pass on the owner array to the multiowned and o_addr := create(_value, add(_code, 0x20), mload(_code))
// the limit to daylimit jumpi(invalidJumpLabel, iszero(extcodesize(o_addr)))
function Wallet(address[] _owners, uint _required, uint _daylimit)
multiowned(_owners, _required) daylimit(_daylimit) {
} }
}
// kills the contract sending everything to `_to`. // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
function kill(address _to) onlymanyowners(sha3(msg.data)) external { // to determine the body of the transaction from the hash provided.
suicide(_to); 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;
} }
}
// gets called when no other function matches // INTERNAL METHODS
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. function clearPending() internal {
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide uint length = m_pendingIndex.length;
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value for (uint i = 0; i < length; ++i)
// and _data arguments). They still get the option of using them if they want, anyways. delete m_txs[m_pendingIndex[i]];
function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 _r) { super.clearPending();
// 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.
_to.call.value(_value)(_data);
return 0;
}
// determine our operation hash.
_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 // FIELDS
// 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 // pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
delete m_txs[m_pendingIndex[i]];
super.clearPending();
}
// FIELDS
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
} }

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { Chip } from 'material-ui'; import { Chip } from 'material-ui';
import IdentityIcon from '../IdentityIcon' ; import IdentityIcon from '../IdentityIcon';
import styles from './chip.css'; import styles from './chip.css';

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
const owners = wallet.owners.map((o) => o.address); // However, wallets can be owned by wallets, that can
// be owned by an account...
let otherWallets = [].concat(_wallets);
let prevLength;
let nextLength;
// Owners ∩ Addresses not null : Wallet is owned // If no more other wallets, or if the size decreased, continue...
// by one of the accounts do {
if (intersection(owners, addresses).length > 0) { prevLength = otherWallets.length;
accounts[wallet.address] = wallet;
} else { otherWallets = otherWallets
contacts[wallet.address] = wallet; .map((wallet) => {
} const addresses = Object.keys(accounts);
const owners = wallet.owners.map((o) => o.address);
// Owners ∩ Addresses not null : Wallet is owned
// by one of the accounts
if (intersection(owners, addresses).length > 0) {
accounts[wallet.address] = wallet;
return false;
}
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,14 +37,26 @@ export default class ModalBox extends Component {
</div> </div>
<div className={ styles.content }> <div className={ styles.content }>
{ this.renderSummary() } { this.renderSummary() }
<div className={ styles.body }> { this.renderBody() }
{ children }
</div>
</div> </div>
</div> </div>
); );
} }
renderBody () {
const { children } = this.props;
if (!children) {
return null;
}
return (
<div className={ styles.body }>
{ children }
</div>
);
}
renderSummary () { renderSummary () {
const { summary } = this.props; const { summary } = this.props;

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,22 +58,13 @@ $widthExpanded: 42%;
} }
} }
&:hover { .item.stretchOn:hover {
.item { flex: 0 0 $widthExpanded;
&.stretchOn { max-width: $widthExpanded;
flex: 0 1 $widthShrunk;
max-width: $widthShrunk;
&:hover {
flex: 0 0 $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,17 +179,36 @@ class Account extends Component {
} }
onClick={ this.store.toggleFundDialog } onClick={ this.store.toggleFundDialog }
/>, />,
<Button isVerifiable
icon={ <VerifyIcon /> } ? (
key='sms-verification' <Button
label={ icon={ <VerifyIcon /> }
<FormattedMessage key='verification'
id='account.button.verify' label={
defaultMessage='verify' <FormattedMessage
id='account.button.verify'
defaultMessage='verify'
/>
}
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
} } } }
/> />
); );