UNPKG

13.6 kBJavaScriptView Raw
1var async = require('async')
2var ethJSUtil = require('ethereumjs-util')
3var BN = ethJSUtil.BN
4var remixLib = require('remix-lib')
5var crypto = require('crypto')
6var TxRunner = remixLib.execution.txRunner
7var txHelper = remixLib.execution.txHelper
8var EventManager = remixLib.EventManager
9var executionContext = remixLib.execution.executionContext
10import { UdappApi } from 'remix-plugin'
11import { EventEmitter } from 'events'
12import * as packageJson from '../package.json'
13
14const profile = {
15 name: 'udapp',
16 displayName: 'universal dapp',
17 description: 'service - run transaction and access account',
18 permission: true,
19 version: packageJson.version
20}
21
22module.exports = class UniversalDApp extends UdappApi {
23
24 constructor (registry) {
25 super(profile)
26 this.events = new EventEmitter()
27 this.event = new EventManager()
28 this._deps = {
29 config: registry.get('config').api
30 }
31
32 this._txRunnerAPI = {
33 config: this._deps.config,
34 detectNetwork: (cb) => {
35 executionContext.detectNetwork(cb)
36 },
37 personalMode: () => {
38 return executionContext.getProvider() === 'web3' ? this._deps.config.get('settings/personal-mode') : false
39 }
40 }
41 this.txRunner = new TxRunner({}, this._txRunnerAPI)
42 this.accounts = {}
43 executionContext.event.register('contextChanged', this.resetEnvironment.bind(this))
44 }
45
46 // TODO : event should be triggered by Udapp instead of TxListener
47 /** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */
48 startListening (txlistener) {
49 txlistener.event.register('newTransaction', (tx) => {
50 this.events.emit('newTransaction', tx)
51 })
52 }
53
54 resetEnvironment () {
55 this.accounts = {}
56 if (executionContext.isVM()) {
57 this._addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511', '0x56BC75E2D63100000')
58 this._addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c', '0x56BC75E2D63100000')
59 this._addAccount('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', '0x56BC75E2D63100000')
60 this._addAccount('d74aa6d18aa79a05f3473dd030a97d3305737cbc8337d940344345c1f6b72eea', '0x56BC75E2D63100000')
61 this._addAccount('71975fbf7fe448e004ac7ae54cad0a383c3906055a65468714156a07385e96ce', '0x56BC75E2D63100000')
62 }
63 // TODO: most params here can be refactored away in txRunner
64 this.txRunner = new TxRunner(this.accounts, {
65 // TODO: only used to check value of doNotShowTransactionConfirmationAgain property
66 config: this._deps.config,
67 // TODO: to refactor, TxRunner already has access to executionContext
68 detectNetwork: (cb) => {
69 executionContext.detectNetwork(cb)
70 },
71 personalMode: () => {
72 return executionContext.getProvider() === 'web3' ? this._deps.config.get('settings/personal-mode') : false
73 }
74 })
75 this.txRunner.event.register('transactionBroadcasted', (txhash) => {
76 executionContext.detectNetwork((error, network) => {
77 if (error || !network) return
78 this.event.trigger('transactionBroadcasted', [txhash, network.name])
79 })
80 })
81 }
82
83 resetAPI (transactionContextAPI) {
84 this.transactionContextAPI = transactionContextAPI
85 }
86
87 /**
88 * Create a VM Account
89 * @param {{privateKey: string, balance: string}} newAccount The new account to create
90 */
91 createVMAccount (newAccount) {
92 const { privateKey, balance } = newAccount
93 if (executionContext.getProvider() !== 'vm') {
94 throw new Error('plugin API does not allow creating a new account through web3 connection. Only vm mode is allowed')
95 }
96 this._addAccount(privateKey, balance)
97 const privKey = Buffer.from(privateKey, 'hex')
98 return '0x' + ethJSUtil.privateToAddress(privKey).toString('hex')
99 }
100
101 newAccount (password, passwordPromptCb, cb) {
102 if (!executionContext.isVM()) {
103 if (!this._deps.config.get('settings/personal-mode')) {
104 return cb('Not running in personal mode')
105 }
106 passwordPromptCb((passphrase) => {
107 executionContext.web3().personal.newAccount(passphrase, cb)
108 })
109 } else {
110 var privateKey
111 do {
112 privateKey = crypto.randomBytes(32)
113 } while (!ethJSUtil.isValidPrivate(privateKey))
114 this._addAccount(privateKey, '0x56BC75E2D63100000')
115 cb(null, '0x' + ethJSUtil.privateToAddress(privateKey).toString('hex'))
116 }
117 }
118
119 _addAccount (privateKey, balance) {
120 if (!executionContext.isVM()) {
121 throw new Error('_addAccount() cannot be called in non-VM mode')
122 }
123
124 if (this.accounts) {
125 privateKey = Buffer.from(privateKey, 'hex')
126 const address = ethJSUtil.privateToAddress(privateKey)
127
128 // FIXME: we don't care about the callback, but we should still make this proper
129 let stateManager = executionContext.vm().stateManager
130 stateManager.getAccount(address, (error, account) => {
131 if (error) return console.log(error)
132 account.balance = balance || '0xf00000000000000001'
133 stateManager.putAccount(address, account, function cb (error) {
134 if (error) console.log(error)
135 })
136 })
137
138 this.accounts['0x' + address.toString('hex')] = { privateKey, nonce: 0 }
139 }
140 }
141
142 getAccounts (cb) {
143 return new Promise((resolve, reject) => {
144 const provider = executionContext.getProvider()
145 switch (provider) {
146 case 'vm': {
147 if (!this.accounts) {
148 if (cb) cb('No accounts?')
149 reject('No accounts?')
150 return
151 }
152 if (cb) cb(null, Object.keys(this.accounts))
153 resolve(Object.keys(this.accounts))
154 }
155 break
156 case 'web3': {
157 if (this._deps.config.get('settings/personal-mode')) {
158 return executionContext.web3().personal.getListAccounts((error, accounts) => {
159 if (cb) cb(error, accounts)
160 if (error) return reject(error)
161 resolve(accounts)
162 })
163 } else {
164 executionContext.web3().eth.getAccounts((error, accounts) => {
165 if (cb) cb(error, accounts)
166 if (error) return reject(error)
167 resolve(accounts)
168 })
169 }
170 }
171 break
172 case 'injected': {
173 executionContext.web3().eth.getAccounts((error, accounts) => {
174 if (cb) cb(error, accounts)
175 if (error) return reject(error)
176 resolve(accounts)
177 })
178 }
179 }
180 })
181 }
182
183 getBalance (address, cb) {
184 address = ethJSUtil.stripHexPrefix(address)
185
186 if (!executionContext.isVM()) {
187 executionContext.web3().eth.getBalance(address, (err, res) => {
188 if (err) {
189 cb(err)
190 } else {
191 cb(null, res.toString(10))
192 }
193 })
194 } else {
195 if (!this.accounts) {
196 return cb('No accounts?')
197 }
198
199 executionContext.vm().stateManager.getAccount(Buffer.from(address, 'hex'), (err, res) => {
200 if (err) {
201 cb('Account not found')
202 } else {
203 cb(null, new BN(res.balance).toString(10))
204 }
205 })
206 }
207 }
208
209 getBalanceInEther (address, callback) {
210 this.getBalance(address, (error, balance) => {
211 if (error) {
212 callback(error)
213 } else {
214 callback(null, executionContext.web3().fromWei(balance, 'ether'))
215 }
216 })
217 }
218
219 pendingTransactionsCount () {
220 return Object.keys(this.txRunner.pendingTxs).length
221 }
222
223 /**
224 * deploy the given contract
225 *
226 * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ).
227 * @param {Function} callback - callback.
228 */
229 createContract (data, confirmationCb, continueCb, promptCb, callback) {
230 this.runTx({data: data, useCall: false}, confirmationCb, continueCb, promptCb, (error, txResult) => {
231 // see universaldapp.js line 660 => 700 to check possible values of txResult (error case)
232 callback(error, txResult)
233 })
234 }
235
236 /**
237 * call the current given contract
238 *
239 * @param {String} to - address of the contract to call.
240 * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ).
241 * @param {Object} funAbi - abi definition of the function to call.
242 * @param {Function} callback - callback.
243 */
244 callFunction (to, data, funAbi, confirmationCb, continueCb, promptCb, callback) {
245 this.runTx({to: to, data: data, useCall: funAbi.constant}, confirmationCb, continueCb, promptCb, (error, txResult) => {
246 // see universaldapp.js line 660 => 700 to check possible values of txResult (error case)
247 callback(error, txResult)
248 })
249 }
250
251 context () {
252 return (executionContext.isVM() ? 'memory' : 'blockchain')
253 }
254
255 getABI (contract) {
256 return txHelper.sortAbiFunction(contract.abi)
257 }
258
259 getFallbackInterface (contractABI) {
260 return txHelper.getFallbackInterface(contractABI)
261 }
262
263 getInputs (funABI) {
264 if (!funABI.inputs) {
265 return ''
266 }
267 return txHelper.inputParametersDeclarationToString(funABI.inputs)
268 }
269
270 /**
271 * This function send a tx only to javascript VM or testnet, will return an error for the mainnet
272 * SHOULD BE TAKEN CAREFULLY!
273 *
274 * @param {Object} tx - transaction.
275 */
276 sendTransaction (tx) {
277 return new Promise((resolve, reject) => {
278 executionContext.detectNetwork((error, network) => {
279 if (error) return reject(error)
280 if (network.name === 'Main' && network.id === '1') {
281 return reject(new Error('It is not allowed to make this action against mainnet'))
282 }
283 this.silentRunTx(tx, (error, result) => {
284 if (error) return reject(error)
285 resolve({
286 transactionHash: result.transactionHash,
287 status: result.result.status,
288 gasUsed: '0x' + result.result.gasUsed.toString('hex'),
289 error: result.result.vm.exceptionError,
290 return: result.result.vm.return ? '0x' + result.result.vm.return.toString('hex') : '0x',
291 createdAddress: result.result.createdAddress ? '0x' + result.result.createdAddress.toString('hex') : undefined
292 })
293 })
294 })
295 })
296 }
297
298 /**
299 * This function send a tx without alerting the user (if mainnet or if gas estimation too high).
300 * SHOULD BE TAKEN CAREFULLY!
301 *
302 * @param {Object} tx - transaction.
303 * @param {Function} callback - callback.
304 */
305 silentRunTx (tx, cb) {
306 if (!executionContext.isVM()) return cb('Cannot silently send transaction through a web3 provider')
307 this.txRunner.rawRun(
308 tx,
309 (network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() },
310 (error, continueTxExecution, cancelCb) => { if (error) { cb(error) } else { continueTxExecution() } },
311 (okCb, cancelCb) => { okCb() },
312 cb
313 )
314 }
315
316 runTx (args, confirmationCb, continueCb, promptCb, cb) {
317 const self = this
318 async.waterfall([
319 function getGasLimit (next) {
320 if (self.transactionContextAPI.getGasLimit) {
321 return self.transactionContextAPI.getGasLimit(next)
322 }
323 next(null, 3000000)
324 },
325 function queryValue (gasLimit, next) {
326 if (args.value) {
327 return next(null, args.value, gasLimit)
328 }
329 if (args.useCall || !self.transactionContextAPI.getValue) {
330 return next(null, 0, gasLimit)
331 }
332 self.transactionContextAPI.getValue(function (err, value) {
333 next(err, value, gasLimit)
334 })
335 },
336 function getAccount (value, gasLimit, next) {
337 if (args.from) {
338 return next(null, args.from, value, gasLimit)
339 }
340 if (self.transactionContextAPI.getAddress) {
341 return self.transactionContextAPI.getAddress(function (err, address) {
342 next(err, address, value, gasLimit)
343 })
344 }
345 self.getAccounts(function (err, accounts) {
346 let address = accounts[0]
347
348 if (err) return next(err)
349 if (!address) return next('No accounts available')
350 if (executionContext.isVM() && !self.accounts[address]) {
351 return next('Invalid account selected')
352 }
353 next(null, address, value, gasLimit)
354 })
355 },
356 function runTransaction (fromAddress, value, gasLimit, next) {
357 var tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp }
358 var payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences }
359 var timestamp = Date.now()
360 if (tx.timestamp) {
361 timestamp = tx.timestamp
362 }
363
364 self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad])
365 self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb,
366 function (error, result) {
367 let eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted')
368 self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad])
369
370 if (error && (typeof (error) !== 'string')) {
371 if (error.message) error = error.message
372 else {
373 try { error = 'error: ' + JSON.stringify(error) } catch (e) {}
374 }
375 }
376 next(error, result)
377 }
378 )
379 }
380 ], cb)
381 }
382}