blockchain.js
1 /*global ethereum*/ 2 import {reduce} from './async'; 3 4 let Blockchain = { 5 list: [], 6 done: false, 7 err: null 8 }; 9 let contracts = []; 10 11 Blockchain.connect = function({dappConnection, dappAutoEnable = true, warnAboutMetamask, blockchainClient = ''}, cb = () => {}) { 12 return new Promise((resolve, reject) => { 13 this.whenEnvIsLoaded(() => { 14 this.doFirst((done) => { 15 this.autoEnable = dappAutoEnable; 16 this.doConnect(dappConnection, { 17 warnAboutMetamask: warnAboutMetamask, 18 blockchainClient: blockchainClient 19 }, (err) => { 20 cb(err); 21 done(err); 22 if (err) { 23 return reject(err); 24 } 25 resolve(err); 26 }); 27 }); 28 }); 29 }); 30 31 }; 32 33 Blockchain.doFirst = function(todo) { 34 todo((err) => { 35 this.done = true; 36 this.err = err; 37 this.list.map((x) => x.apply(x, [self.err])); 38 }); 39 }; 40 41 Blockchain.whenEnvIsLoaded = function(cb) { 42 if (typeof document !== 'undefined' && document !== null && !(/comp|inter|loaded/).test(document.readyState)) { 43 document.addEventListener('DOMContentLoaded', cb); 44 } else { 45 cb(); 46 } 47 }; 48 49 Blockchain.Providers = {}; 50 51 Blockchain.registerProvider = function(providerName, obj) { 52 this.Providers[providerName] = obj; 53 }; 54 55 Blockchain.setProvider = function(providerName, options) { 56 let provider = this.Providers[providerName]; 57 58 if (!provider) { 59 throw new Error('Unknown blockchain provider. ' + 60 'Make sure to register it first using EmbarkJS.Blockchain.registerProvider(providerName, providerObject'); 61 } 62 63 this.currentProviderName = providerName; 64 this.blockchainConnector = provider; 65 66 provider.init(options); 67 }; 68 69 Blockchain.doConnect = function(connectionList, opts, doneCb) { 70 const self = this; 71 72 const checkConnect = (next) => { 73 this.blockchainConnector.getAccounts((error, _a) => { 74 const provider = self.blockchainConnector.getCurrentProvider(); 75 const connectionString = provider.host; 76 77 if (error) this.blockchainConnector.setProvider(null); 78 79 return next(null, { 80 connectionString, 81 error, 82 connected: !error 83 }); 84 }); 85 }; 86 87 const connectWeb3 = async (next) => { 88 const connectionString = 'web3://'; 89 90 if (window.ethereum) { 91 try { 92 if (Blockchain.autoEnable) { 93 await ethereum.enable(); 94 } 95 this.blockchainConnector.setProvider(ethereum); 96 return checkConnect(next); 97 } catch (error) { 98 return next(null, { 99 connectionString, 100 error, 101 connected: false 102 }); 103 } 104 } 105 106 return next(null, { 107 connectionString, 108 error: new Error("web3 provider not detected"), 109 connected: false 110 }); 111 }; 112 113 const connectWebsocket = (value, next) => { 114 this.blockchainConnector.setProvider(this.blockchainConnector.getNewProvider('WebsocketProvider', value)); 115 checkConnect(next); 116 }; 117 118 const connectHttp = (value, next) => { 119 this.blockchainConnector.setProvider(this.blockchainConnector.getNewProvider('HttpProvider', value)); 120 checkConnect(next); 121 }; 122 123 let connectionErrs = {}; 124 125 this.doFirst(function(cb) { 126 reduce(connectionList, false, function(result, connectionString, next) { 127 if (result.connected) { 128 return next(null, result); 129 } else if(result) { 130 connectionErrs[result.connectionString] = result.error; 131 } 132 133 if (connectionString === '$WEB3') { 134 connectWeb3(next); 135 } else if (connectionString.indexOf('ws://') >= 0) { 136 connectWebsocket(connectionString, next); 137 } else { 138 connectHttp(connectionString, next); 139 } 140 }, function(_err, _connectionErr, _connected) { 141 self.blockchainConnector.getAccounts((err, accounts) => { 142 const currentProv = self.blockchainConnector.getCurrentProvider(); 143 if (opts.warnAboutMetamask && currentProv && currentProv.isMetaMask) { 144 // if we are using metamask, ask embark to turn on dev_funds 145 // embark will only do this if geth is our client and we are in 146 // dev mode 147 if(opts.blockchainClient === 'geth') { 148 console.warn("%cNote: There is a known issue with Geth that may cause transactions to get stuck when using Metamask. Please log in to the cockpit (http://localhost:8000/embark?enableRegularTxs=true) to enable a workaround. Once logged in, the workaround will automatically be enabled.", "font-size: 2em"); 149 } 150 if(opts.blockchainClient === 'parity') { 151 console.warn("%cNote: Parity blocks the connection from browser extensions like Metamask. To resolve this problem, go to https://embark.status.im/docs/blockchain_configuration.html#Using-Parity-and-Metamask", "font-size: 2em"); 152 } 153 console.warn("%cNote: Embark has detected you are in the development environment and using Metamask, please make sure Metamask is connected to your local node", "font-size: 2em"); 154 } 155 if (accounts) { 156 self.blockchainConnector.setDefaultAccount(accounts[0]); 157 } 158 159 const connectionError = new BlockchainConnectionError(connectionErrs); 160 161 cb(connectionErrs); 162 doneCb(connectionErrs); 163 }); 164 }); 165 }); 166 }; 167 168 Blockchain.enableEthereum = function() { 169 if (window.ethereum) { 170 return ethereum.enable().then((accounts) => { 171 this.blockchainConnector.setProvider(ethereum); 172 this.blockchainConnector.setDefaultAccount(accounts[0]); 173 contracts.forEach(contract => { 174 contract.options.from = this.blockchainConnector.getDefaultAccount(); 175 }); 176 return accounts; 177 }); 178 } 179 }; 180 181 Blockchain.execWhenReady = function(cb) { 182 if (this.done) { 183 return cb(this.err, this.web3); 184 } 185 if (!this.list) { 186 this.list = []; 187 } 188 this.list.push(cb); 189 }; 190 191 Blockchain.doFirst = function(todo) { 192 var self = this; 193 todo(function(err) { 194 self.done = true; 195 self.err = err; 196 if (self.list) { 197 self.list.map((x) => x.apply(x, [self.err, self.web3])); 198 } 199 }); 200 }; 201 202 let Contract = function(options) { 203 var self = this; 204 var ContractClass; 205 206 this.abi = options.abi; 207 this.address = options.address; 208 this.gas = options.gas; 209 this.code = '0x' + options.code; 210 211 this.web3 = options.web3; 212 this.blockchainConnector = Blockchain.blockchainConnector; 213 214 ContractClass = this.blockchainConnector.newContract({abi: this.abi, address: this.address}); 215 contracts.push(ContractClass); 216 ContractClass.options.data = this.code; 217 const from = this.from || self.blockchainConnector.getDefaultAccount() || this.web3.eth.defaultAccount; 218 if (from) { 219 ContractClass.options.from = from; 220 } 221 ContractClass.abi = ContractClass.options.abi; 222 ContractClass.address = this.address; 223 ContractClass.gas = this.gas; 224 225 let originalMethods = Object.keys(ContractClass); 226 227 Blockchain.execWhenReady(function(_err, _web3) { 228 if (!ContractClass.currentProvider) { 229 ContractClass.setProvider(self.blockchainConnector.getCurrentProvider() || self.web3.currentProvider); 230 } 231 ContractClass.options.from = self.blockchainConnector.getDefaultAccount() ||self.web3.eth.defaultAccount; 232 }); 233 234 ContractClass._jsonInterface.forEach((abi) => { 235 if (originalMethods.indexOf(abi.name) >= 0) { 236 console.log(abi.name + " is a reserved word and cannot be used as a contract method, property or event"); 237 return; 238 } 239 240 if (!abi.inputs) { 241 return; 242 } 243 244 let numExpectedInputs = abi.inputs.length; 245 246 if (abi.type === 'function' && abi.constant) { 247 ContractClass[abi.name] = function() { 248 let ref = ContractClass.methods[abi.name]; 249 let call = ref.apply(ref, ...arguments).call; 250 return call.apply(call, []); 251 }; 252 } else if (abi.type === 'function') { 253 ContractClass[abi.name] = function() { 254 let options = {}, cb = null, args = Array.from(arguments || []).slice(0, numExpectedInputs); 255 if (typeof (arguments[numExpectedInputs]) === 'function') { 256 cb = arguments[numExpectedInputs]; 257 } else if (typeof (arguments[numExpectedInputs]) === 'object') { 258 options = arguments[numExpectedInputs]; 259 cb = arguments[numExpectedInputs + 1]; 260 } 261 262 let ref = ContractClass.methods[abi.name]; 263 let send = ref.apply(ref, args).send; 264 return send.apply(send, [options, cb]); 265 }; 266 } else if (abi.type === 'event') { 267 ContractClass[abi.name] = function(options, cb) { 268 let ref = ContractClass.events[abi.name]; 269 return ref.apply(ref, [options, cb]); 270 }; 271 } 272 }); 273 274 return ContractClass; 275 }; 276 277 Contract.prototype.deploy = function(args, _options) { 278 var self = this; 279 var contractParams; 280 var options = _options || {}; 281 282 contractParams = args || []; 283 284 contractParams.push({ 285 from: this.blockchainConnector.getDefaultAccount() || this.web3.eth.accounts[0], 286 data: this.code, 287 gas: options.gas || 800000 288 }); 289 290 291 const contractObject = this.blockchainConnector.newContract({abi: this.abi}); 292 293 return new Promise(function (resolve, reject) { 294 contractParams.push(function(err, transaction) { 295 if (err) { 296 reject(err); 297 } else if (transaction.address !== undefined) { 298 resolve(new Contract({ 299 abi: self.abi, 300 code: self.code, 301 address: transaction.address 302 })); 303 } 304 }); 305 306 contractObject["new"].apply(contractObject, contractParams); 307 }); 308 }; 309 310 Contract.prototype.new = Contract.prototype.deploy; 311 312 Contract.prototype.at = function(address) { 313 return new Contract({abi: this.abi, code: this.code, address: address}); 314 }; 315 316 Contract.prototype.send = function(value, unit, _options) { 317 let options, wei; 318 if (typeof unit === 'object') { 319 options = unit; 320 wei = value; 321 } else { 322 options = _options || {}; 323 wei = this.blockchainConnector.toWei(value, unit); 324 } 325 326 options.to = this.address; 327 options.value = wei; 328 329 return this.blockchainConnector.send(options); 330 }; 331 332 Blockchain.Contract = Contract; 333 334 class BlockchainConnectionError extends Error { 335 constructor(connectionErrors) { 336 super("Could not establish a connection to a node."); 337 338 this.connections = connectionErrors; 339 this.name = 'BlockchainConnectionError'; 340 } 341 } 342 343 export default Blockchain;