├── package.js ├── .versions ├── README.md └── accounts.js /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "ethereum:accounts", 3 | summary: 4 | "Provides and updates the ethereum accounts in the Accounts collection", 5 | version: "1.1.0", 6 | git: "http://github.com/ethereum/meteor-package-accounts" 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom("1.0"); 11 | api.use("underscore", ["client", "server"]); 12 | api.use("mongo", ["client", "server"]); 13 | 14 | api.use("frozeman:persistent-minimongo@0.1.8", "client"); 15 | api.use("ethereum:web3@1.0.0-beta.33", ["client", "server"]); 16 | 17 | api.export(["EthAccounts"], ["client", "server"]); 18 | 19 | api.addFiles("accounts.js", ["client", "server"]); 20 | }); 21 | 22 | // Package.onTest(function(api) { 23 | // api.use('tinytest'); 24 | // api.use('ethereum:accounts'); 25 | // api.addFiles('accounts-tests.js'); 26 | // }); 27 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | amplify@1.0.0 3 | babel-compiler@7.0.5 4 | babel-runtime@1.2.2 5 | base64@1.0.10 6 | binary-heap@1.0.10 7 | boilerplate-generator@1.4.0 8 | callback-hook@1.1.0 9 | check@1.3.0 10 | ddp@1.4.0 11 | ddp-client@2.3.2 12 | ddp-common@1.4.0 13 | ddp-server@2.1.2 14 | diff-sequence@1.1.0 15 | dynamic-import@0.3.0 16 | ecmascript@0.10.5 17 | ecmascript-runtime@0.5.0 18 | ecmascript-runtime-client@0.6.2 19 | ecmascript-runtime-server@0.5.0 20 | ejson@1.1.0 21 | ethereum:accounts@1.0.0 22 | ethereum:web3@1.0.0-beta.33 23 | frozeman:persistent-minimongo@0.1.8 24 | geojson-utils@1.0.10 25 | http@1.4.0 26 | id-map@1.1.0 27 | jquery@1.11.10 28 | logging@1.1.19 29 | meteor@1.8.2 30 | minimongo@1.4.3 31 | modules@0.11.5 32 | modules-runtime@0.9.2 33 | mongo@1.4.2 34 | mongo-dev-server@1.1.0 35 | mongo-id@1.0.7 36 | npm-mongo@2.2.34 37 | ordered-dict@1.1.0 38 | promise@0.10.2 39 | random@1.1.0 40 | reload@1.2.0 41 | retry@1.1.0 42 | routepolicy@1.0.13 43 | socket-stream-client@0.1.0 44 | tracker@1.1.3 45 | underscore@1.0.10 46 | url@1.2.0 47 | webapp@1.5.0 48 | webapp-hashing@1.0.9 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum accounts 2 | 3 | Provides you with an `EthAccounts` collection, where balances are automatically updated. 4 | Additionally the accounts are persisted in localstorage. 5 | 6 | If the ethereum node removes accounts, 7 | the `EthAccounts` collection will set the `deactivated: true` property to these accounts and hide them from normal queries. 8 | 9 | If the Accounts should reapear in the node (e.g. the user importet those, or mist allwed them access), they will be available again, 10 | including all the extra properties you've set. 11 | 12 | **Note** don't use the `EthAccounts` collection to add your own custom accounts as a reload of your application, 13 | or any change in `web3.eth.accounts` would hide them. 14 | 15 | ## Installation 16 | 17 | $ meteor add ethereum:accounts 18 | 19 | ## Usage 20 | 21 | Initialize Accounts on the start of your application, as soon as you have a ethereum connection: 22 | 23 | ```js 24 | EthAccounts.init(); 25 | ``` 26 | 27 | Then simply use the global `EthAccounts` object like any other minimongo collection. 28 | It provides the `.find()`, `.findOne()`, `.findAll()`, `.update()`, `.updateAll()` and `.remove()` functions e.g.: 29 | 30 | ```js 31 | // Get all active accounts 32 | var myAccounts = EthAccounts.find().fetch(); 33 | 34 | [ 35 | { 36 | "_id": "2Zd3Z9XQrc7iN7Ci3" 37 | "address": "0x343c98e2b6e49bc0fed722c2a269f3814ddd1533", 38 | "balance": "18260939861619682985678", 39 | "name": "Coinbase", 40 | } 41 | ] 42 | 43 | // or 44 | var myPrimaryAccount = EthAccounts.findOne({name: 'Coinbase'}); 45 | ``` 46 | 47 | #### If you want to get truly all accounts including the deactivated ones use: 48 | 49 | ```js 50 | var allAccounts = EthAccounts.findAll().fetch(); 51 | 52 | [ 53 | { 54 | "_id": "2Zd3Z9XQrc7iN7Ci3" 55 | "address": "0x343c98e2b6e49bc0fed722c2a269f3814ddd1533", 56 | "balance": "18260939861619682985678", 57 | "name": "Coinbase", 58 | }, 59 | { 60 | "_id": "56sbC8dggbYstmN2o", 61 | "address": "0x990ccf8a0de58091c028d6ff76bb235ee67c1c39", 62 | "balance": "0", 63 | "name": "0x990ccf8a0de58091c028d6ff76bb235ee67c1c39", 64 | "deactivated": true 65 | } 66 | ] 67 | ``` 68 | 69 | #### If you want to update a deactivated account use: 70 | 71 | ```js 72 | EthAccounts.updateAll({address: "0x990ccf8a0de58091c028d6ff76bb235ee67c1c39"}, {name: 'XYZ'}}); 73 | ``` 74 | 75 | #### If you manually want to activate an account to make it visible call: 76 | 77 | ```js 78 | EthAccounts.updateAll( 79 | { address: "0x990ccf8a0de58091c028d6ff76bb235ee67c1c39" }, 80 | { $unset: { deactivated: "" } } 81 | ); 82 | ``` 83 | -------------------------------------------------------------------------------- /accounts.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @module Ethereum:accounts 4 | */ 5 | 6 | /** 7 | The accounts collection, with some ethereum additions. 8 | 9 | @class EthAccounts 10 | @constructor 11 | */ 12 | var collection = new Mongo.Collection("ethereum_accounts", { 13 | connection: null 14 | }); 15 | EthAccounts = _.clone(collection); 16 | EthAccounts._collection = collection; 17 | 18 | if (typeof PersistentMinimongo !== "undefined") 19 | new PersistentMinimongo(EthAccounts._collection); 20 | 21 | /** 22 | Updates the accounts balances, by watching for new blocks and checking the balance. 23 | 24 | @method _watchBalance 25 | */ 26 | EthAccounts._watchBalance = function() { 27 | var _this = this; 28 | 29 | if (this.blockSubscription) { 30 | this.blockSubscription.unsubscribe(); 31 | } 32 | 33 | // UPDATE SIMPLE ACCOUNTS balance on each new block 34 | this.blockSubscription = web3.eth 35 | .subscribe("newBlockHeaders") 36 | .on("data", function() { 37 | _this._updateBalance(); 38 | }); 39 | }; 40 | 41 | /** 42 | Updates the accounts balances. 43 | 44 | @method _updateBalance 45 | */ 46 | EthAccounts._updateBalance = function() { 47 | var _this = this; 48 | 49 | _.each(EthAccounts.find({}).fetch(), function(account) { 50 | web3.eth.getBalance(account.address, function(err, res) { 51 | if (!err) { 52 | if (res.toFixed) { 53 | res = res.toFixed(); 54 | } 55 | 56 | EthAccounts.update(account._id, { 57 | $set: { 58 | balance: res 59 | } 60 | }); 61 | } 62 | }); 63 | }); 64 | }; 65 | 66 | /** 67 | Updates the accounts list, 68 | if its finds a difference between the accounts in the collection and the accounts in the accounts array. 69 | 70 | @method _addAccounts 71 | */ 72 | EthAccounts._addAccounts = function() { 73 | var _this = this; 74 | 75 | // UPDATE normal accounts on start 76 | web3.eth.getAccounts(function(e, accounts) { 77 | if (!e) { 78 | var visibleAccounts = _.pluck(EthAccounts.find().fetch(), "address"); 79 | 80 | if ( 81 | !_.isEmpty(accounts) && 82 | _.difference(accounts, visibleAccounts).length === 0 && 83 | _.difference(visibleAccounts, accounts).length === 0 84 | ) 85 | return; 86 | 87 | var localAccounts = EthAccounts.findAll().fetch(); 88 | 89 | // if the accounts are different, update the local ones 90 | _.each(localAccounts, function(account) { 91 | // needs to have the balance 92 | if (!account.balance) return; 93 | 94 | // set status deactivated, if it seem to be gone 95 | if (!_.contains(accounts, account.address)) { 96 | EthAccounts.updateAll(account._id, { 97 | $set: { 98 | deactivated: true 99 | } 100 | }); 101 | } else { 102 | EthAccounts.updateAll(account._id, { 103 | $unset: { 104 | deactivated: "" 105 | } 106 | }); 107 | } 108 | 109 | accounts = _.without(accounts, account.address); 110 | }); 111 | 112 | // ADD missing accounts 113 | var accountsCount = visibleAccounts.length + 1; 114 | _.each(accounts, function(address) { 115 | web3.eth.getBalance(address, function(e, balance) { 116 | if (!e) { 117 | if (balance.toFixed) { 118 | balance = balance.toFixed(); 119 | } 120 | 121 | web3.eth.getCoinbase(function(error, coinbase) { 122 | if (error) { 123 | console.warn("getCoinbase error: ", error); 124 | coinbase = null; // continue with null coinbase 125 | } 126 | 127 | var doc = EthAccounts.findAll({ 128 | address: address 129 | }).fetch()[0]; 130 | 131 | var insert = { 132 | type: "account", 133 | address: address, 134 | balance: balance, 135 | name: 136 | address === coinbase 137 | ? "Main account (Etherbase)" 138 | : "Account " + accountsCount 139 | }; 140 | 141 | if (doc) { 142 | EthAccounts.updateAll(doc._id, { 143 | $set: insert 144 | }); 145 | } else { 146 | EthAccounts.insert(insert); 147 | } 148 | 149 | if (address !== coinbase) accountsCount++; 150 | }); 151 | } 152 | }); 153 | }); 154 | } 155 | }); 156 | }; 157 | 158 | /** 159 | Builds the query with the addition of "{deactivated: {$exists: false}}" 160 | 161 | @method _addToQuery 162 | @param {Mixed} arg 163 | @param {Object} options 164 | @param {Object} options.includeDeactivated If set then de-activated accounts are also included. 165 | @return {Object} The query 166 | */ 167 | EthAccounts._addToQuery = function(args, options) { 168 | var _this = this; 169 | 170 | options = _.extend( 171 | { 172 | includeDeactivated: false 173 | }, 174 | options 175 | ); 176 | 177 | var args = Array.prototype.slice.call(args); 178 | 179 | if (_.isString(args[0])) { 180 | args[0] = { 181 | _id: args[0] 182 | }; 183 | } else if (!_.isObject(args[0])) { 184 | args[0] = {}; 185 | } 186 | 187 | if (!options.includeDeactivated) { 188 | args[0] = _.extend(args[0], { 189 | deactivated: { $exists: false } 190 | }); 191 | } 192 | 193 | return args; 194 | }; 195 | 196 | /** 197 | Find all accounts, besides the deactivated ones 198 | 199 | @method find 200 | @return {Object} cursor 201 | */ 202 | EthAccounts.find = function() { 203 | return this._collection.find.apply(this, this._addToQuery(arguments)); 204 | }; 205 | 206 | /** 207 | Find all accounts, including the deactivated ones 208 | 209 | @method findAll 210 | @return {Object} cursor 211 | */ 212 | EthAccounts.findAll = function() { 213 | return this._collection.find.apply( 214 | this, 215 | this._addToQuery(arguments, { 216 | includeDeactivated: true 217 | }) 218 | ); 219 | }; 220 | 221 | /** 222 | Find one accounts, besides the deactivated ones 223 | 224 | @method findOne 225 | @return {Object} cursor 226 | */ 227 | EthAccounts.findOne = function() { 228 | return this._collection.findOne.apply(this, this._addToQuery(arguments)); 229 | }; 230 | 231 | /** 232 | Update accounts, besides the deactivated ones 233 | 234 | @method update 235 | @return {Object} cursor 236 | */ 237 | EthAccounts.update = function() { 238 | return this._collection.update.apply(this, this._addToQuery(arguments)); 239 | }; 240 | 241 | /** 242 | Update accounts, including the deactivated ones 243 | 244 | @method updateAll 245 | @return {Object} cursor 246 | */ 247 | EthAccounts.updateAll = function() { 248 | return this._collection.update.apply( 249 | this, 250 | this._addToQuery(arguments, { 251 | includeDeactivated: true 252 | }) 253 | ); 254 | }; 255 | 256 | /** 257 | Update accounts, including the deactivated ones 258 | 259 | @method upsert 260 | @return {Object} cursor 261 | */ 262 | EthAccounts.upsert = function() { 263 | return this._collection.upsert.apply( 264 | this, 265 | this._addToQuery(arguments, { 266 | includeDeactivated: true 267 | }) 268 | ); 269 | }; 270 | 271 | /** 272 | Starts fetching and watching the accounts 273 | 274 | @method init 275 | */ 276 | EthAccounts.init = function() { 277 | var _this = this; 278 | 279 | if (typeof web3 === "undefined") { 280 | console.warn( 281 | "EthAccounts couldn't find web3, please make sure to instantiate a web3 object before calling EthAccounts.init()" 282 | ); 283 | return; 284 | } 285 | 286 | Tracker.nonreactive(function() { 287 | _this._addAccounts(); 288 | 289 | _this._updateBalance(); 290 | _this._watchBalance(); 291 | 292 | // check for new accounts every 2s 293 | Meteor.clearInterval(_this._intervalId); 294 | _this._intervalId = Meteor.setInterval(function() { 295 | _this._addAccounts(); 296 | }, 2000); 297 | }); 298 | }; 299 | --------------------------------------------------------------------------------