/ src / blockchain.js
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;