diff --git a/contracts/Invitation.sol b/contracts/Invitation.sol index c33eb76..01a51a4 100644 --- a/contracts/Invitation.sol +++ b/contracts/Invitation.sol @@ -6,6 +6,9 @@ contract Invitation { User public inviter; User public invitee; + event InvitationAccepted(); + event InvitationRejected(); + modifier inviteeOnly() { require(msg.sender == address(invitee)); _ ; @@ -19,9 +22,11 @@ contract Invitation { function accept() public inviteeOnly { inviter.invitationAccepted(); + InvitationAccepted(); } function reject() public inviteeOnly { inviter.invitationRejected(); + InvitationRejected(); } } \ No newline at end of file diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol index 0a2399e..37bbf00 100644 --- a/contracts/Migrations.sol +++ b/contracts/Migrations.sol @@ -18,8 +18,8 @@ contract Migrations { last_completed_migration = completed; } - function upgrade(address new_address) public restricted { - Migrations upgraded = Migrations(new_address); + function upgrade(address newAddress) public restricted { + Migrations upgraded = Migrations(newAddress); upgraded.setCompleted(last_completed_migration); } } diff --git a/contracts/User.sol b/contracts/User.sol index 6c589d8..1a15c27 100644 --- a/contracts/User.sol +++ b/contracts/User.sol @@ -6,16 +6,21 @@ import "./Invitation.sol"; contract User { struct Profile { string name; - string avatar; + bytes32 avatar; //Swarm hash } struct WhisperInfo { - string pubKey; - string key; + bytes pubKey; + bytes key; } event UserProfileUpdated(); event WhisperInfoUpdated(); + event InvitationSent(Invitation invitation, User to); + event InvitationReceived(Invitation invitation, User from); + event ContactAdded(User contact); + event ContactRemoved(User contact); + event OwnerChanged(address from, address to); Profile private _profile; address private _owner; @@ -32,12 +37,12 @@ contract User { _ ; } - modifier iscontact() { + modifier isContactOrOwner() { var found = false; for (uint i = 0; i < _contacts.length && !found; i++) { found = _contacts[i] == msg.sender; } - require(found); + require(found || msg.sender == _owner); _ ; } @@ -47,11 +52,11 @@ contract User { registry = UserRegistry(msg.sender); } - function getProfileInfo() public view returns (string name, string avatar) { + function getProfileInfo() public view returns (string name, bytes32 avatar) { return (_profile.name, _profile.avatar); } - function setProfileInfo(string name, string avatar) public onlyowner { + function setProfileInfo(string name, bytes32 avatar) public onlyowner { _profile.name = name; _profile.avatar = avatar; UserProfileUpdated(); @@ -66,17 +71,18 @@ contract User { var oldOwner = _owner; _owner = newOwner; registry.ownerUpdated(oldOwner, newOwner); + OwnerChanged(oldOwner, newOwner); } - function getWhisperPubKey() public view iscontact returns (string) { + function getWhisperPubKey() public view isContactOrOwner returns (bytes) { return _whisperInfo.pubKey; } - function getWhisperInfo() public view onlyowner returns (string pubKey, string key) { + function getWhisperInfo() public view onlyowner returns (bytes pubKey, bytes key) { return (_whisperInfo.pubKey, _whisperInfo.key); } - function setWhisperInfo(string pubKey, string key) public onlyowner { + function setWhisperInfo(bytes pubKey, bytes key) public onlyowner { _whisperInfo.key = key; _whisperInfo.pubKey = pubKey; WhisperInfoUpdated(); @@ -102,6 +108,7 @@ contract User { } _contacts.push(contact); + ContactAdded(contact); } function removeContactPrivate(User contact) private { @@ -122,6 +129,7 @@ contract User { function removeContact(User contact) external onlyowner { removeContactPrivate(contact); contact.removeMe(); + ContactRemoved(contact); } function removeMe() public { @@ -129,9 +137,10 @@ contract User { User contact = User(msg.sender); removeContactPrivate(contact); + ContactRemoved(contact); } - function sendInvitation(User invitee) external onlyowner returns(Invitation) { + function sendInvitation(User invitee) external onlyowner { require(address(invitee) != 0x0); require(this != invitee); @@ -145,7 +154,7 @@ contract User { invitee.receiveInvitation(invitation); - return invitation; + InvitationSent(invitation, invitee); } function receiveInvitation(Invitation invitation) public { @@ -159,6 +168,7 @@ contract User { } _inbox.push(invitation); + InvitationReceived(invitation, invitation.inviter()); } //TODO: withdrawInvitation diff --git a/contracts/UserRegistry.sol b/contracts/UserRegistry.sol index afe3db1..4716c4d 100644 --- a/contracts/UserRegistry.sol +++ b/contracts/UserRegistry.sol @@ -5,15 +5,19 @@ import "./User.sol"; contract UserRegistry { mapping(address => User) private users; + event UserRegistered(User user); + event UserOwnerUpdated(User user, address newOwner); + function me() view external returns (User) { return users[msg.sender]; } - function register() external returns (User) { + function register() external { + require(msg.sender != 0x0); require(address(users[msg.sender]) == 0x0); var user = new User(msg.sender); users[msg.sender] = user; - return user; + UserRegistered(user); } function getUser(address walletId) public view returns (User) { @@ -28,5 +32,6 @@ contract UserRegistry { require(user == msg.sender); users[oldWalletId] = User(0x0); users[newWalletId] = user; + UserOwnerUpdated(user, newWalletId); } } \ No newline at end of file diff --git a/migrations/1_initial_migration.js b/migrations/1_initial_migration.js index 4d5f3f9..6e0a9f1 100644 --- a/migrations/1_initial_migration.js +++ b/migrations/1_initial_migration.js @@ -1,4 +1,4 @@ -var Migrations = artifacts.require("./Migrations.sol"); +var Migrations = artifacts.require("Migrations"); module.exports = function(deployer) { deployer.deploy(Migrations); diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index e25b8be..a6d7978 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -1,14 +1,13 @@ -var User = artifacts.require("./User.sol"); -var UserRegistry = artifacts.require("./UserRegistry.sol"); -var Invitation = artifacts.require("./Invitation.sol"); +var User = artifacts.require("User"); +var UserRegistry = artifacts.require("UserRegistry"); +var Invitation = artifacts.require("Invitation"); module.exports = function(deployer, network, accounts) { - return deployer.deploy(UserRegistry) - .then(function() { - return deployer.deploy(User, accounts[0]); - }) - .then(function() { - return deployer.deploy(Invitation, User.address, User.address); - }); + return deployer.deploy([ + [User, accounts[0]], + UserRegistry + ]).then(function() { + return deployer.deploy(Invitation, User.address, User.address) + }); }; diff --git a/package.json b/package.json index 6cf553a..07ebaf9 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,12 @@ "recursive-readdir": "2.1.0", "strip-ansi": "3.0.1", "style-loader": "0.13.1", - "truffle-contract": "^1.1.8", + "swarm-js": "^0.1.37", + "truffle": "^4.0.6", + "truffle-contract": "^3.0.3", "truffle-solidity-loader": "0.0.8", "url-loader": "0.5.7", + "web3": "^0.20.4", "webpack": "1.14.0", "webpack-dev-server": "1.16.2", "webpack-manifest-plugin": "1.1.0", @@ -58,7 +61,9 @@ "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js --env=jsdom", - "truffle": "truffle" + "truffle": "truffle", + "swarm": "~/Library/Application\\ Support/Mist/swarmjs/bin/swarm -datadir ~/Library/Ethereum/rinkeby --ens-api ~/Library/Ethereum/rinkeby/geth.ipc --corsdomain '*' --bzzaccount $ACCOUNT", + "geth": "~/Library/Application\\ Support/Mist/binaries/Geth/unpacked/geth --rpc --shh --rpcport 9545 --rpccorsdomain '*' --rinkeby --rpcapi eth,web3,shh,net --unlock $ACCOUNT" }, "jest": { "collectCoverageFrom": [ diff --git a/src/App.js b/src/App.js index 87b674d..007ffa5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import SimpleStorageContract from '../build/contracts/SimpleStorage.json' +import API from './api' import getWeb3 from './utils/getWeb3' import './css/oswald.css' @@ -12,58 +12,70 @@ class App extends Component { super(props) this.state = { - storageValue: 0, - web3: null + web3: null, + userRegistry: null, + user: null, + chatSocket: null, + userId: "" } } componentWillMount() { // Get network provider and web3 instance. // See utils/getWeb3 for more info. + getWeb3.then((web3) => { - getWeb3 - .then(results => { - this.setState({ - web3: results.web3 + let api = API(web3) + this.setState(() => { + return { + web3: web3, + userRegistry: api.UserRegistry.instance() + } }) - // Instantiate contract once web3 provided. - this.instantiateContract() - }) - .catch(() => { - console.log('Error finding web3.') - }) - } + api.Accounts.instance().getAccounts() + .then((accounts) => { // Search for account with registered user. If not found - register with 0 account + let found = accounts.find((value) => value.user != null) + if (found) { + api.Accounts.instance().currentAccount = found.id + return found + } else { + let account = accounts[0] + return api.UserRegistry.instance().register().then((user) => { + account.user = user + return account + }) + } + }) + .then((account) => { + console.log("Account", account) + let socket = new api.Whisper(account.user) + this.setState(() => { + return { + user: account.user, + chatSocket: socket, + userId: account.user.id + } + }) - instantiateContract() { - /* - * SMART CONTRACT EXAMPLE - * - * Normally these functions would be called in the context of a - * state management library, but for convenience I've placed them here. - */ + socket.on('error', (err) => { + console.error('Socket error', err) + }) - const contract = require('truffle-contract') - const simpleStorage = contract(SimpleStorageContract) - simpleStorage.setProvider(this.state.web3.currentProvider) + socket.on('message', (message) => { + console.log('New message', message) + }) - // Declaring this for later so we can chain functions on SimpleStorage. - var simpleStorageInstance + socket.on('started', () => { + console.log('Socket started') + socket.sendMessage(account.user, "TEST MESSAGE") + }) - // Get accounts. - this.state.web3.eth.getAccounts((error, accounts) => { - simpleStorage.deployed().then((instance) => { - simpleStorageInstance = instance - - // Stores a given value, 5 by default. - return simpleStorageInstance.set(5, {from: accounts[0]}) - }).then((result) => { - // Get the value from the contract to prove it worked. - return simpleStorageInstance.get.call(accounts[0]) - }).then((result) => { - // Update state with the result. - return this.setState({ storageValue: result.c[0] }) - }) + socket.start() + }) + .catch((err) => { + console.error("Error", err) + }) }) } @@ -82,7 +94,7 @@ class App extends Component {
If your contracts compiled and migrated successfully, below will show a stored value of 5 (by default).
Try changing the value stored on line 59 of App.js.
-The stored value is: {this.state.storageValue}
+The stored value is: {this.state.userId}
diff --git a/src/api/Accounts.js b/src/api/Accounts.js new file mode 100644 index 0000000..fcca172 --- /dev/null +++ b/src/api/Accounts.js @@ -0,0 +1,38 @@ + +import promisify from '../utils/promisify' + +class Accounts { + static $inject = ['Web3()', 'UserRegistry()'] + + constructor(web3, userRegistry) { + this._web3 = web3 + this._userRegistry = userRegistry + } + + _checkDefault(accounts) { + if (!this.currentAccount) { + if (accounts.length === 0) throw new Error("NO_ACCOUNT") + this.currentAccount = accounts[0] + } + } + + getAccounts() { + return promisify(this._web3.eth, 'getAccounts')().then((accounts) => { + console.log("Accounts", accounts) + this._checkDefault(accounts) + return Promise.all(accounts.map((account) => { + return this._userRegistry.me(account).then((user) => { return { id: account, user: user } }) + })) + }) + } + + get currentAccount() { + return this._web3.eth.defaultAccount + } + + set currentAccount(walledAccountId) { + this._web3.eth.defaultAccount = walledAccountId + } +} + +export default Accounts \ No newline at end of file diff --git a/src/api/Invitation.js b/src/api/Invitation.js index 124f7f6..de56466 100644 --- a/src/api/Invitation.js +++ b/src/api/Invitation.js @@ -1,36 +1,48 @@ import contract from 'truffle-contract' import InvitationContract from '../../build/contracts/Invitation.json' -import getWeb3 from '../utils/getWeb3' +import EventEmitter from 'events' -const invitationContract = contract(InvitationContract) +class Invitation extends EventEmitter { + static $inject = ['User', '_InvitationContract()'] -class Invitation { - constructor(invitationId, fromMe) { + constructor(User, Contract, invitationId, fromMe) { + super() this.id = invitationId - this._fromMe = fromMe - this._invitationContract = invitationContract.at(invitationId) + this.fromMe = fromMe + this._invitationContract = Contract.at(invitationId) + this._User = User this._user = null + this.__setupEvents() } - getUser() { - if (this._user) return this._user - return this._user = this._invitationContract.then((invitation) => this._fromMe ? invitation.invitee() : invitation.inviter()) + __setupEvents() { + this._invitationContract.then((inv) => { + inv.InvitationAccepted().watch((err, response) => { + this.emit('accepted') + }) + inv.InvitationRejected().watch((err, response) => { + this.emit('rejected') + }) + }) } - accept() { - return this._invitationContract.then((invitation) => invitation.accept()) + getUser() { + if (this._user) return this._user + return this._user = this._invitationContract + .then((invitation) => this.fromMe ? invitation.invitee() : invitation.inviter()) + .then((userId) => new this._User(userId)) } - reject() { - return this._invitationContract.then((invitation) => invitation.reject()) + static injected(context) { + const invitationContract = contract(InvitationContract) + invitationContract.setProvider(this.context.injected('Web3()').currentProvider) + let defaults = invitationContract.defaults() + if (!defaults.gas) { + defaults.gas = this.context.injected('GAS') + invitationContract.defaults(defaults) + } + this.context.addSingletonObject('_InvitationContract', invitationContract) } } -export default (invitationId, fromMe) => { - return getWeb3().then((result) => { - if (invitationContract.getProvider() !== result.web3.currentProvider) { - invitationContract.setProvider(result.web3.currentProvider); - } - return new Invitation(invitationId, fromMe) - }) -} \ No newline at end of file +export default Invitation \ No newline at end of file diff --git a/src/api/Swarm.js b/src/api/Swarm.js new file mode 100644 index 0000000..481725b --- /dev/null +++ b/src/api/Swarm.js @@ -0,0 +1,31 @@ + +import promisify from '../utils/promisify' +import swarm from 'swarm-js' + +class Swarm { + static $inject = ['Web3()', 'SWARM_API_URL'] + + constructor(web3, url) { + if (web3.bzz) { + this._bzz = web3.bzz + this._emulated = false + } else { + this._bzz = swarm.at(url) + this._emulated = true + } + } + + uploadFile() { + return this._emulated ? this._bzz.upload({ pick: 'file' }) : promisify(this._bzz, 'upload')({ pick: 'file' }) + } + + upload(data) { + return this._emulated ? this._bzz.upload(data) : promisify(this._bzz, 'upload')(data) + } + + download(hash) { + return this._emulated ? this._bzz.download(hash) : promisify(this._bzz, 'download')(hash) + } +} + + export default Swarm diff --git a/src/api/User.js b/src/api/User.js index ab1167b..b48a84c 100644 --- a/src/api/User.js +++ b/src/api/User.js @@ -1,31 +1,50 @@ import contract from 'truffle-contract' import UserContract from '../../build/contracts/User.json' -import getWeb3 from '../utils/getWeb3' -import Invitation from './Invitation' +import EventEmitter from 'events' -const userContract = contract(UserContract) +const NULL_ID = '0x0000000000000000000000000000000000000000' -class User { - constructor (userId) { - this._userContract = userContract.at(userId) +class User extends EventEmitter { + static $inject = ['Invitation', '_UserContract()'] + + constructor (Invitation, UserContract, userId) { + super() + this._userContract = UserContract.at(userId) + this._Invitation = Invitation this.id = userId + this._sentInvitations = null + this._inboxInvitations = null + this._pubKey = null + this._profile = null + this._whisperInfo = null + this._contacts = null + this.__setupEvents() + } + + __setupEvents() { this._userContract.then((user) => { - user.UserProfileUpdated().watch((err, response) => { + user.UserProfileUpdated().watch((err, event) => { this._profile = null + this.emit("profileUpdated") }) - user.WhisperInfoUpdated().watch((err, response) => { + user.WhisperInfoUpdated().watch((err, event) => { this._pubKey = null this._whisperInfo = null - if (this.whisperInfoUpdated) this.whisperInfoUpdated() + this.emit("whisperInfoUpdated") + }) + user.ContactAdded().watch((err, event) => { + this.emit('contactAdded', new this.constructor(event.args.contact)) + }) + user.ContactRemoved().watch((err, event) => { + this.emit('contactRemoved', new this.constructor(event.args.contact)) + }) + user.OwnerChanged().watch((err, event) => { + this.emit('ownerChanged', event.to) + }) + user.InvitationReceived().watch((err, event) => { + this.emit('invitationReceived', new this._Invitation(event.invitation, false)) }) }) - this._sentInvitations = null - this._inboxInvitations = null - this.whisperInfoUpdated = null - this._pubKey = null - this._profile = null - this._whisperInfo = null - this._contacts = null } getWhisperInfo() { @@ -45,48 +64,66 @@ class User { getProfile() { if (this._profile) return this._profile return this._profile = this._userContract.then((user) => user.getProfileInfo()) + .then(([name, avatar]) => [name, avatar.slice(2)]) } setProfile(name, avatar) { - return this._userContract.then((user) => user.setProfileInfo(name, avatar)) + return this._userContract.then((user) => user.setProfileInfo(name, (avatar && avatar !== '') ? '0x'+avatar : avatar)) } invite(user) { return this._userContract - .then((user) => user.sendInvitation(user.id)) - .then((result) => { + .then((contract) => contract.sendInvitation(user.id).then((result) => !result.logs[0] ? null: result.logs[0].args.invitation)) + .then((invitationId) => { + if (!invitationId || invitationId === NULL_ID) throw new Error("Invitation failed") this._sentInvitations = null - return result + return new this._Invitation(invitationId, true) }) } + acceptInvitation(invitation) { + return this._userContract + .then((user) => user.acceptInvitation(invitation.id)) + .then(() => undefined) + } + + rejectInvitation(invitation) { + return this._userContract + .then((user) => user.rejectInvitation(invitation.id)) + .then(() => undefined) + } + getSentInvitations() { if (this._sentInvitations) return this._sentInvitations return this._sentInvitations = this._userContract .then((user) => user.getSentInvitations()) - .then((invitations) => invitations.map((i) => Invitation(i, true))) + .then((invitations) => invitations.map((i) => new this._Invitation(i, true))) } getInboxInvitations() { if (this._inboxInvitations) return this._inboxInvitations return this._inboxInvitations = this._userContract .then((user) => user.getInboxInvitations()) - .then((invitations) => invitations.map((i) => Invitation(i, false))) + .then((invitations) => invitations.map((i) => new this._Invitation(i, false))) } getContacts() { if (this._contacts) return this._contacts return this._contacts = this._userContract .then((user) => user.getContacts()) - .then((contacts) => contacts.map((c) => new User(c))) + .then((contacts) => contacts.map((c) => new this.constructor(c))) } -} -export default (userId) => { - return getWeb3().then((result) => { - if (userContract.getProvider() !== result.web3.currentProvider) { - userContract.setProvider(result.web3.currentProvider) + static injected() { + const userContract = contract(UserContract) + userContract.setProvider(this.context.injected('Web3()').currentProvider) + let defaults = userContract.defaults() + if (!defaults.gas) { + defaults.gas = this.context.injected('GAS') + userContract.defaults(defaults) } - return new User(userId) - }) -} \ No newline at end of file + this.context.addSingletonObject('_UserContract', userContract) + } +} + +export default User \ No newline at end of file diff --git a/src/api/UserRegistry.js b/src/api/UserRegistry.js index 6f8c4fb..e83c667 100644 --- a/src/api/UserRegistry.js +++ b/src/api/UserRegistry.js @@ -1,43 +1,55 @@ import contract from 'truffle-contract' import UserRegistryContract from '../../build/contracts/UserRegistry.json' -import getWeb3 from '../utils/getWeb3' -import User from './User' -const userRegistryContract = contract(UserRegistryContract) - -var userRegistry = null; +const NULL_ID = '0x0000000000000000000000000000000000000000' class UserRegistry { - constructor(userRegistryContract) { - this._userRegistryContract = userRegistryContract - this._me = null + + static $inject = ['User', '_UserRegistryContract()'] + + constructor(User, userRegistryContract) { + this._userRegistryContract = userRegistryContract.deployed() + this._User = User } meOrRegister() { - if (this._me) return this._me - return this._me = this._userRegistryContract - .then((registry) => registry.me().then((me) => [me, registry])) - .then(([me, registry]) => (me === '0x00000000000000000000') ? registry.register().then(() => registry.me()) : me) - .then((me) => User(me)) + return this.me().then((user) => user ? user : this.register()) + } + + me(walletId) { + return this._userRegistryContract + .then((registry) => registry.me(walletId ? { from: walletId } : undefined)) + .then((userId) => (userId === NULL_ID) ? null : new this._User(userId)) + } + + register() { + return this._userRegistryContract + .then((registry) => registry.register()).then((result) => (!result.logs[0]) ? null : result.logs[0].args.user) + .then((userId) => { + if (!userId || userId === NULL_ID) throw new Error("Registration failed") + return new this._User(userId) + }) } getUser(walletId) { return this._userRegistryContract .then((registry) => registry.getUser(walletId)) .then((userId) => { - if (userId === '0x00000000000000000000') throw new Error('User not found') - return User(userId) + if (userId === NULL_ID || userId === '0x') throw new Error('User not found') + return new this._User(userId) }) } -} -export default () => { - if (userRegistry) return Promise.resolve(userRegistry) - return getWeb3().then((result) => { - if (userRegistryContract.getProvider() !== result.web3.currentProvider) { - userRegistryContract.setProvider(result.web3.currentProvider) + static injected(context) { + const userRegistryContract = contract(UserRegistryContract) + userRegistryContract.setProvider(this.context.injected('Web3()').currentProvider) + let defaults = userRegistryContract.defaults() + if (!defaults.gas) { + defaults.gas = this.context.injected('GAS') + userRegistryContract.defaults(defaults) } - userRegistry = new UserRegistry(userRegistryContract.deployed()) - return userRegistry - }) -} \ No newline at end of file + this.context.addSingletonObject('_UserRegistryContract', userRegistryContract) + } +} + +export default UserRegistry \ No newline at end of file diff --git a/src/api/WhisperSocket.js b/src/api/WhisperSocket.js index f5f9495..36baefb 100644 --- a/src/api/WhisperSocket.js +++ b/src/api/WhisperSocket.js @@ -1,95 +1,122 @@ -import getWeb3 from '../utils/getWeb3' +import EventEmitter from 'events' import promisify from '../utils/promisify' -class WhisperSocket { - constructor(user) { - this.user = user; - this.onmessage = null; - this.onerror = null; - this._listening = null; +class WhisperSocket extends EventEmitter { + static $inject = ['Web3()'] - this.user.whisperInfoUpdated = () => { - this.initialize().catch((err) => { - console.error(err) - if (this.onerror) this.onerror(err) - }) - } - } + constructor(web3, user) { + super() + + this._user = user + this._shh = web3.shh + this._web3 = web3 + this._filter = null - initialize() { - return this._updateIdentity() - .then(([pubKey, key]) => { - return this._listen(pubKey, key).then(() => this) + user.on('whisperInfoUpdated', () => { + this._infoUpdated() }) } - _listen(pubKey, key) { - return this._getShh().then((shh) => { - if (this._listening) this._listening.stopWatching() - this._listening = shh.filter({ to: key }) - this._listening.watch((err, message) => { + start() { + if (this._timer) { + this.emit("error", new Error("Already started")) + return + } + this._updateIdentity() + .then(([pubKey, key]) => this._listen(pubKey, key)) + .then(() => { + this.emit("started") + }) + .catch((err) => { + this.emit("error", err) + }) + } + + stop() { + if (this._filter) { + this._filter.stopWatching((err) => { if (err) { - console.error(err) - if (this.onerror) this.onerror(err) - } else { - console.debug('Message arrived', message) - if (this.onmessage) { - this.onmessage({ - from: message.from, - message: message.payload, - sent: message.sent - }) - } + this.emit('error', err) } + this.emit('stopped') }) - }) + this._filter = null + } } - _updateIdentity() { - this.user.getWhisperInfo() - .then((info) => this._getShh().then((shh) => [info, shh])) - .then(([info, shh]) => { - return info.key ? promisify(shh.hasIdentity)(info.key).then((has) => [info.key, has, shh]) : [info.key, false, shh] - }) - .then(([key, has, shh]) => { - return has ? [key, false, shh] : promisify(shh.newIdentity)().then((id) => [id, true, shh]) + _infoUpdated() { + if (this._filter) { + this.stop() + this.start() + } + } + + _encodeKey(key) { + return Promise.resolve((key && key !== '') ? '0x' + key : key) + } + + _decodeKey(key) { + return Promise.resolve(key.slice(2)) + } + + _listen(pubKey, key) { + console.log("Listen", pubKey, key) + if (this._filter) throw new Error("Already listening" ) + + this._filter = this._shh.newMessageFilter({ + privateKeyID: key }) - .then(([id, created, shh]) => { - // This part for new web3 1.0 API - // shh.getPublicKey(id).then((pubKey) => { - // if (created) { - // return this.user.setWhisperInfo(pubKey, id).then(() => [pubKey, id]) - // } - // return [pubKey, id] - // }) - if (created) { - return this.user.setWhisperInfo(id, id).then(() => [id, id]) + + this._filter.watch((err, message) => { + if (err) { + console.error(err) + this.emit("error", err) + } else { + console.debug('New messages arrived', message) + this.emit("message", { + from: message.sig, + message: this._web3.toUtf8(message.payload), + sent: new Date(message.timestamp*1000) + }) } - return [id, id] }) } - _getShh() { - return getWeb3().then((result) => result.web3.shh); + _updateIdentity() { + return this._user.getWhisperInfo() + .then(([pubKey, key]) => this._decodeKey(key).then((decoded) => [pubKey, decoded])) + .then(([pubKey, key]) => { + return key !== '' ? promisify(this._shh, 'hasKeyPair')(key).then((has) => [key, pubKey, has]) : [key, pubKey, false] + }) + .then(([key, pubKey, has]) => { + return has ? [key, pubKey, false] : promisify(this._shh, 'newKeyPair')().then((id) => [id, pubKey, true]) + }) + .then(([id, pubKey, created]) => { + if (created) { + return promisify(this._shh, 'getPublicKey')(id).then((pubKey) => { + return this._encodeKey(id).then((encoded) => this._user.setWhisperInfo(pubKey, encoded).then(() => [pubKey, id])) + }) + } + return [pubKey, id] + }) } sendMessage(to, message) { return to.getPubKey().then((pubKey) => { - return this.user.getWhisperInfo().then((info) => [info.key, pubKey]) + return this._user.getWhisperInfo() + .then(([ownPubKey, ownKey]) => this._decodeKey(ownKey).then((decoded) => [decoded, pubKey])) }).then(([key, pubKey]) => { - return this._getShh().then((shh) => { - return promisify(shh.post)({ - from: key, - payload: message, + return promisify(this._shh, 'post')({ + sig: key, + pubKey: pubKey, + payload: this._web3.fromUtf8(message), ttl: 100, - workToProve: 10 + powTarget: 0.5, + powTime: 2 }) }) - }) } } -export default (user) => { - return (new WhisperSocket(user)).initialize() -} \ No newline at end of file +export default WhisperSocket \ No newline at end of file diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..a6112f2 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,24 @@ +import UserRegistry from './UserRegistry' +import User from './User' +import Invitation from './Invitation' +import Swarm from './Swarm' +import Whisper from './WhisperSocket' +import Accounts from './Accounts' +import Context from '../utils/di' + +export default (web3, swarm_api_url = 'http://swarm-gateways.net') => { + let context = new Context() + context.addConstant('GAS', 3141592) + context.addConstant('SWARM_API_URL', swarm_api_url) + + context.addSingletonObject('Web3', web3) + + context.addClass('Invitation', Invitation) + context.addClass('User', User) + context.addClass('Accounts', Accounts, true) + context.addClass('UserRegistry', UserRegistry, true) + context.addClass('Swarm', Swarm, true) + context.addClass('Whisper', Whisper) + + return context.storage +} \ No newline at end of file diff --git a/src/api/swarm.js b/src/api/swarm.js deleted file mode 100644 index 524417f..0000000 --- a/src/api/swarm.js +++ /dev/null @@ -1,27 +0,0 @@ - -import getWeb3 from '../utils/getWeb3' -import promisify from '../utils/promisify' - -class Swarm { - constructor(bzz) { - this._bzz = bzz - } - - uploadFile() { - return promisify(this._bzz.upload)({ pick: 'file' }) - } - - download(hash) { - return promisify(this._bzz.download)(hash) - } -} - -var swarm = null - -export default () => { - if (swarm) return Promise.resolve(swarm) - return getWeb3().then((result) => { - swarm = new Swarm(result.web3.bzz) - return swarm - }) -} diff --git a/src/utils/di.js b/src/utils/di.js new file mode 100644 index 0000000..5f90ed1 --- /dev/null +++ b/src/utils/di.js @@ -0,0 +1,50 @@ + +class Context { + constructor() { + this.storage = {} + } + + addClass(name, injectable, singleton) { + const _context = this + const injected = class Injected extends injectable { + static context = _context + + constructor(...args) { + let injects = (injectable.$inject || []).map((inj) => _context.injected(inj)) + super(...(injects.concat(args))) + } + } + if (singleton) { + let _instance = null + + injected.instance = function() { + if (_instance) return _instance + return _instance = new this() + } + } + this.storage[name] = injected + if (typeof injected.injected === 'function') { + injected.injected() + } + } + + addConstant(name, value) { + this.storage[name] = value + } + + addSingletonObject(name, object) { + this.storage[name] = { instance: () => object } + } + + injected(name) { + var object = false + if (name.slice(-2) === '()') { + name = name.slice(0, -2) + object = true + } + var injectable = this.storage[name] + return object ? (typeof injectable.instance === 'function' ? injectable.instance() : new injectable()) : injectable + } +} + +export default Context \ No newline at end of file diff --git a/src/utils/getWeb3.js b/src/utils/getWeb3.js index 4a25eae..d24cc0b 100644 --- a/src/utils/getWeb3.js +++ b/src/utils/getWeb3.js @@ -3,7 +3,6 @@ import Web3 from 'web3' let getWeb3 = new Promise(function(resolve, reject) { // Wait for loading completion to avoid race conditions with web3 injection timing. window.addEventListener('load', function() { - var results var web3 = window.web3 // Checking if Web3 has been injected by the browser (Mist/MetaMask) @@ -11,27 +10,20 @@ let getWeb3 = new Promise(function(resolve, reject) { // Use Mist/MetaMask's provider. web3 = new Web3(web3.currentProvider) - results = { - web3: web3 - } - console.log('Injected web3 detected.'); - resolve(results) + resolve(web3) } else { // Fallback to localhost if no web3 injection. We've configured this to // use the development console's port by default. var provider = new Web3.providers.HttpProvider('http://127.0.0.1:9545') web3 = new Web3(provider) - - results = { - web3: web3 - } + web3.bzz = null // We can't work with fallback bzz in this version console.log('No web3 instance injected, using Local web3.'); - resolve(results) + resolve(web3) } }) }) diff --git a/src/utils/promisify.js b/src/utils/promisify.js index 6d40149..155bbdf 100644 --- a/src/utils/promisify.js +++ b/src/utils/promisify.js @@ -1,14 +1,18 @@ -export default (func) => { +export default (func_or_obj, funcName) => { return (...args) => { return new Promise((resolve, reject) => { - let newArgs = args.concat([(...results) => { - if (results[0]) { - reject(results[0]) + let newArgs = args.concat([(err, result) => { + if (err) { + reject(err) } else { - resolve(results.slice(1)) + resolve(result) } }]) - func(...newArgs) + if (funcName) { + func_or_obj[funcName](...newArgs) + } else { + func_or_obj(...newArgs) + } }) } } \ No newline at end of file diff --git a/truffle.js b/truffle.js index a6330d6..c9cc9a6 100644 --- a/truffle.js +++ b/truffle.js @@ -1,4 +1,20 @@ module.exports = { // See