UNPKG

@gnosis.pm/pm-js

Version:

A javascript library for building applications on top of Gnosis, the Ethereum prediction market platform

302 lines (261 loc) 13.4 kB
import _ from 'lodash' import TruffleContract from 'truffle-contract' import Web3 from 'web3' import IPFS from 'ipfs-mini' import * as lmsr from './lmsr' import * as utils from './utils' import * as oracles from './oracles' import * as events from './events' import * as markets from './markets' const windowLoaded = new Promise((accept, reject) => { if(typeof window === 'undefined') return accept() if(typeof window.addEventListener !== 'function') return reject(new Error('expected to be able to register event listener')) window.addEventListener('load', function loadHandler(event) { window.removeEventListener('load', loadHandler, false) return accept(event) }, false) }) const gasStatsData = require('@gnosis.pm/pm-contracts/build/gas-stats.json') const gasLimit = 4e6 const gasDefaultMaxMultiplier = 2 const implementationInterfaceMap = { StandardMarket: ['Market'], } const contractArtifacts = [ 'Math', 'Event', 'CategoricalEvent', 'ScalarEvent', 'EventFactory', 'ERC20', 'HumanFriendlyToken', 'WETH9', 'CentralizedOracle', 'CentralizedOracleFactory', 'UltimateOracle', 'UltimateOracleFactory', 'LMSRMarketMaker', 'Market', 'StandardMarket', 'StandardMarketFactory', ].map((name) => require(`@gnosis.pm/pm-contracts/build/contracts/${name}.json`)) const instanceModules = [oracles, events, markets] /** * Represents the Gnosis Prediction Markets JS Library API */ class Gnosis { /** * Factory function for asynchronously creating an instance of the API * * Note: this method is asynchronous and will return a Promise * * @param {string|Provider} [opts.ethereum] - An instance of a Web3 provider or a URL of a Web3 HTTP provider. If not specified, Web3 provider will be either the browser-injected Web3 (Mist/MetaMask) or an HTTP provider looking at http://localhost:8545 * @param {string} [opts.defaultAccount] - The account to use as the default `from` address for ethereum transactions conducted through the Web3 instance. If unspecified, will be the first account found on Web3. See Gnosis.setWeb3Provider `defaultAccount` parameter for more info. * @param {Object} [opts.ipfs] - ipfs-mini configuration object * @param {string} [opts.ipfs.host='ipfs.infura.io'] - IPFS node address * @param {Number} [opts.ipfs.port=5001] - IPFS protocol port * @param {string} [opts.ipfs.protocol='https'] - IPFS protocol name * @param {Function} [opts.logger] - A callback for logging. Can also provide 'console' to use `console.log`. * @returns {Gnosis} An instance of the pm.js API */ static async create (opts) { opts = _.defaultsDeep(opts || {}, { ipfs: { host: 'ipfs.infura.io', port: 5001, protocol: 'https' } }) let gnosis = new Gnosis(opts) await gnosis.initialized(opts) return gnosis } /** * **Warning:** Do not use constructor directly. Some asynchronous initialization will not be handled. Instead, use Gnosis.create. * @constructor */ constructor (opts) { // Logger setup const { logger } = opts this.log = logger == null ? () => {} : logger === 'console' ? console.log : logger // IPFS instantiation this.ipfs = utils.promisifyAll(new IPFS(opts.ipfs)) /** * A collection of Truffle contract abstractions for the following Gnosis contracts: * * - `Math <https://gnosis-pm-contracts.readthedocs.io/en/latest/Math.html>`_ * - `Event <https://gnosis-pm-contracts.readthedocs.io/en/latest/Event.html>`_ * - `CategoricalEvent <https://gnosis-pm-contracts.readthedocs.io/en/latest/CategoricalEvent.html>`_ * - `ScalarEvent <https://gnosis-pm-contracts.readthedocs.io/en/latest/ScalarEvent.html>`_ * - `EventFactory <https://gnosis-pm-contracts.readthedocs.io/en/latest/EventFactory.html>`_ * - `HumanFriendlyToken <https://gnosis-pm-contracts.readthedocs.io/en/latest/HumanFriendlyToken.html>`_ * - `CentralizedOracle <https://gnosis-pm-contracts.readthedocs.io/en/latest/CentralizedOracle.html>`_ * - `CentralizedOracleFactory <https://gnosis-pm-contracts.readthedocs.io/en/latest/CentralizedOracleFactory.html>`_ * - `UltimateOracle <https://gnosis-pm-contracts.readthedocs.io/en/latest/UltimateOracle.html>`_ * - `UltimateOracleFactory <https://gnosis-pm-contracts.readthedocs.io/en/latest/UltimateOracleFactory.html>`_ * - `LMSR Market Maker <https://gnosis-pm-contracts.readthedocs.io/en/latest/LMSRMarketMaker.html>`_ * - `Market <https://gnosis-pm-contracts.readthedocs.io/en/latest/Market.html>`_ * - `StandardMarket <https://gnosis-pm-contracts.readthedocs.io/en/latest/StandardMarket.html>`_ * - `Standard Market Factory <https://gnosis-pm-contracts.readthedocs.io/en/latest/StandardMarketFactory.html>`_ * - `ERC20 <https://theethereum.wiki/w/index.php/ERC20_Token_Standard>`_ * - `WETH9 <https://weth.io/>`_ * * These are configured to use the web3 provider specified in Gnosis.create or subsequently modified with Gnosis.setWeb3Provider. The default gas costs for these abstractions are set to the maximum cost of their respective entries found in the `gas-stats.json` file built from the `core contracts <https://github.com/gnosis/pm-contracts#readme>`_. Additionally, the default message sender (i.e. `from` address) is set via the optional `defaultAccount` param in Gnosis.setWeb3Provider. * * @member {Object} Gnosis#contracts */ this.contracts = _.fromPairs(contractArtifacts.map((artifact) => { const c = TruffleContract(artifact) const name = c.contract_name if(gasStatsData[name] != null) { c.prototype.gasStats = gasStatsData[name] c.addProp('gasStats', () => gasStatsData[name]) } return [name, c] })) this.contracts.Token = this.contracts.ERC20 this.contracts.EtherToken = this.contracts.WETH9 _.forOwn(this.contracts, (c, name, cs) => { const maxGasCost = Math.max( ...Object.values(c.gasStats || {}).map( (fnStats) => fnStats.max != null ? fnStats.max.gasUsed : -Infinity), ..._.flatMap(implementationInterfaceMap[name] || [], (implName) => Object.values(cs[implName].gasStats || {}).map( (fnStats) => fnStats.max != null ? fnStats.max.gasUsed : -Infinity)) ) if(maxGasCost > 0) { c.defaults({ gas: Math.min(gasLimit, (gasDefaultMaxMultiplier * maxGasCost) | 0) }) } }) this.TruffleContract = TruffleContract this.instanceNames = {} instanceModules.forEach((module) => { Object.keys(module).forEach((instanceProp) => { if( this[instanceProp] != null && typeof this[instanceProp].estimateGas === 'function' ) { this[instanceProp].estimateGas = this[instanceProp].estimateGas.bind(this) } }) }) } async initialized (opts) { await this.setWeb3Provider(opts.ethereum, opts.defaultAccount) } /** * Setter for the ethereum web3 provider. * * Note: this method is asynchronous and will return a Promise * * @param {string|Provider} [provider] - An instance of a Web3 provider or a URL of a Web3 HTTP provider. If not specified, Web3 provider will be either the browser-injected Web3 (Mist/MetaMask) or an HTTP provider looking at http://localhost:8545 * @param {string} [defaultAccount] - An address to be used as the default `from` account for conducting transactions using the associated Web3 instance. If not specified, will be inferred from Web3 using the first account obtained by `web3.eth.getAccounts`. If no such account exists, default account will not be set. */ async setWeb3Provider (provider, defaultAccount) { if (provider == null) { // Prefer Web3 injected by the browser (Mist/MetaMask) // Window must be loaded first so that there isn't a race condition for resolving injected Web3 instance await windowLoaded if (typeof web3 !== 'undefined') { this.web3 = new Web3(web3.currentProvider) } else { this.web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) } } else if (typeof provider === 'string') { this.web3 = new Web3(new Web3.providers.HttpProvider(provider)) } else if ( typeof provider === 'object' && typeof provider.send === 'function' ) { if(typeof provider.sendAsync !== 'function') { provider.sendAsync = provider.send } this.web3 = new Web3(provider) } else { throw new TypeError(`provider of type '${typeof provider}' not supported`) } _.forOwn(this.contracts, (c) => { c.setProvider(this.web3.currentProvider) }) if(defaultAccount == null) { const accounts = await utils.promisify(this.web3.eth.getAccounts)() if (accounts.length > 0) { this.setDefaultAccount(accounts[0]) } } else { this.setDefaultAccount(defaultAccount) } await Promise.all([ /** * This will be a WETH9 contract abstraction pointing to the canonical `wETH <https://weth.io/>`_ on the current network. * * @member {Contract} Gnosis#etherToken */ this.trySettingContractInstance('etherToken', this.contracts.WETH9), /** * If `StandardMarketFactory <https://gnosis-pm-contracts.readthedocs.io/en/latest/StandardMarketFactory.html>`_ is deployed to the current network, this will be set to an StandardMarketFactory contract abstraction pointing at the deployment address. * * @member {Contract} Gnosis#standardMarketFactory */ this.trySettingContractInstance('standardMarketFactory', this.contracts.StandardMarketFactory), /** * If `LMSRMarketMaker <https://gnosis-pm-contracts.readthedocs.io/en/latest/LMSRMarketMaker.html>`_ is deployed to the current network, this will be set to an LMSRMarketMaker contract abstraction pointing at the deployment address. * * @member {Contract} Gnosis#lmsrMarketMaker */ this.trySettingContractInstance('lmsrMarketMaker', this.contracts.LMSRMarketMaker), ..._.toPairs(this.instanceNames).map(([cName, iName]) => this.trySettingContractInstance(iName, this.contracts[cName])), ]) } async trySettingContractInstance(instanceName, contract) { try { this[instanceName] = await contract.deployed() } catch(e) { delete this[instanceName] if(!e.message.includes('has not been deployed to detected network')) { throw e } } } /** * Imports contracts into this Gnosis instance using an object mapping contract names to their corresponding Truffle artifacts. Additionally, attempt to set attributes on the Gnosis instance corresponding to `instanceNames`. * * Note: this method is asynchronous and will return a Promise * * @param {Object} artifacts - Object mapping contract names to Truffle artifacts. * @param {Object} instanceNames - Object mapping contract names to the name of an attribute on the Gnosis instance which will represent the deployed version of the contract, analogous to :attr:`standardMarketFactory` or :attr:`lmsrMarketMaker`. */ async importContracts(artifacts, instanceNames) { _.forOwn(artifacts, (artifact, name) => { if(this.contracts[name]) { throw new Error(`custom contract ${ name } already exists in contract set!`) } const contract = TruffleContract(artifact) contract.setProvider(this.web3.currentProvider) contract.defaults({ from: this.defaultAccount }) this.contracts[name] = contract }) Object.assign(this.instanceNames, instanceNames) await Promise.all(_.toPairs(instanceNames).map(([cName, iName]) => this.trySettingContractInstance(iName, this.contracts[cName]))) } setDefaultAccount (account) { /** * The default account to be used as the `from` address for transactions done with this Gnosis instance. If there is no account, this will not be set. * * @member {string} Gnosis#defaultAccount */ this.defaultAccount = account _.forOwn(this.contracts, (c) => { c.defaults({ from: account }) }) } } _.assign(Gnosis.prototype, ...instanceModules) _.assign(Gnosis, lmsr, utils) export default Gnosis