UNPKG

incubed

Version:

Typescript-version of the incubed client

576 lines 24.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const ethereumjs_abi_1 = require("ethereumjs-abi"); const ethereumjs_util_1 = require("ethereumjs-util"); const ETx = require("ethereumjs-tx"); const util_1 = require("../../util/util"); const serialize_1 = require("./serialize"); const BN = require("bn.js"); class API { constructor(client) { this.client = client; } send(name, ...params) { return this.client.sendRPC(name, params || []) .then(r => { if (r.error) throw new Error(r.error.message || r.error); return r.result; }); } /** * Returns the number of most recent block. (as number) */ blockNumber() { return this.send('eth_blockNumber').then(parseInt); } /** * Returns the current price per gas in wei. (as number) */ gasPrice() { return this.send('eth_gasPrice').then(parseInt); } /** * Executes a new message call immediately without creating a transaction on the block chain. */ call(tx, block = 'latest') { return this.send('eth_call', tx, toHexBlock(block)); } /** * Executes a function of a contract, by passing a [method-signature](https://github.com/ethereumjs/ethereumjs-abi/blob/master/README.md#simple-encoding-and-decoding) and the arguments, which will then be ABI-encoded and send as eth_call. */ callFn(to, method, ...args) { const t = createCallParams(method, args || []); return this.send('eth_call', { to, data: t.txdata }, 'latest').then(t.convert); } /** * Returns the EIP155 chain ID used for transaction signing at the current best block. Null is returned if not available. */ chainId() { return this.send('eth_chainId'); } /** * Makes a call or transaction, which won’t be added to the blockchain and returns the used gas, which can be used for estimating the used gas. */ estimateGas(tx /*, block: BlockType = 'latest'*/) { return this.send('eth_estimateGas', tx /*, toHexBlock(block)*/).then(parseInt); } /** * Returns the balance of the account of given address in wei (as hex). */ getBalance(address, block = 'latest') { return this.send('eth_getBalance', address, toHexBlock(block)).then(util_1.toBN); } /** * Returns code at a given address. */ getCode(address, block = 'latest') { return this.send('eth_getCode', address, block); } /** * Returns the value from a storage position at a given address. */ getStorageAt(address, pos, block = 'latest') { return this.send('eth_getStorageAt', address, pos, toHexBlock(block)); } /** * Returns information about a block by hash. */ getBlockByHash(hash, includeTransactions = false) { return this.send('eth_getBlockByHash', hash, includeTransactions); } /** * Returns information about a block by block number. */ getBlockByNumber(block = 'latest', includeTransactions = false) { return this.send('eth_getBlockByNumber', toHexBlock(block), includeTransactions); } /** * Returns the number of transactions in a block from a block matching the given block hash. */ getBlockTransactionCountByHash(block) { return this.send('eth_getBlockTransactionCountByHash', block).then(parseInt); } /** * Returns the number of transactions in a block from a block matching the given block number. */ getBlockTransactionCountByNumber(block) { return this.send('eth_getBlockTransactionCountByNumber', block).then(parseInt); } /** * Polling method for a filter, which returns an array of logs which occurred since last poll. */ getFilterChanges(id) { return this.send('eth_getFilterChanges', id); } /** * Returns an array of all logs matching filter with given id. */ getFilterLogs(id) { return this.send('eth_getFilterLogs', id); } /** * Returns an array of all logs matching a given filter object. */ getLogs(filter) { if (filter.fromBlock) filter.fromBlock = toHexBlock(filter.fromBlock); if (filter.toBlock) filter.toBlock = toHexBlock(filter.toBlock); if (filter.limit) filter.limit = util_1.toNumber(filter.limit); return this.send('eth_getLogs', filter); } /** * Returns information about a transaction by block hash and transaction index position. */ getTransactionByBlockHashAndIndex(hash, pos) { return this.send('eth_getTransactionByBlockHashAndIndex', hash, pos); } /** * Returns information about a transaction by block number and transaction index position. */ getTransactionByBlockNumberAndIndex(block, pos) { return this.send('eth_getTransactionByBlockNumberAndIndex', toHexBlock(block), pos); } /** * Returns the information about a transaction requested by transaction hash. */ getTransactionByHash(hash) { return this.send('eth_getTransactionByHash', hash); } /** * Returns the number of transactions sent from an address. (as number) */ getTransactionCount(address, block = 'latest') { return this.send('eth_getTransactionCount', address, block).then(parseInt); } /** * Returns the receipt of a transaction by transaction hash. * Note That the receipt is available even for pending transactions. */ getTransactionReceipt(hash) { return this.send('eth_getTransactionReceipt', hash).then(_ => !_ ? null : (Object.assign({}, _, { contractAddress: _.contractAddress && ethereumjs_util_1.toChecksumAddress(_.contractAddress), from: _.from && ethereumjs_util_1.toChecksumAddress(_.from) }))); } /** * Returns information about a uncle of a block by hash and uncle index position. * Note: An uncle doesn’t contain individual transactions. */ getUncleByBlockHashAndIndex(hash, pos) { return this.send('eth_getUncleByBlockHashAndIndex', hash, pos); } /** * Returns information about a uncle of a block number and uncle index position. * Note: An uncle doesn’t contain individual transactions. */ getUncleByBlockNumberAndIndex(block, pos) { return this.send('eth_getUncleByBlockNumberAndIndex', block, pos); } /** * Returns the number of uncles in a block from a block matching the given block hash. */ getUncleCountByBlockHash(hash) { return this.send('eth_getUncleCountByBlockHash', hash).then(parseInt); } /** * Returns the number of uncles in a block from a block matching the given block hash. */ getUncleCountByBlockNumber(block) { return this.send('eth_getUncleCountByBlockNumber', block).then(parseInt); } /** * Creates a filter in the node, to notify when a new block arrives. To check if the state has changed, call eth_getFilterChanges. */ newBlockFilter() { return this.send('eth_newBlockFilter'); } /** * Creates a filter object, based on filter options, to notify when the state changes (logs). To check if the state has changed, call eth_getFilterChanges. * * A note on specifying topic filters: * Topics are order-dependent. A transaction with a log with topics [A, B] will be matched by the following topic filters: * * [] “anything” * [A] “A in first position (and anything after)” * [null, B] “anything in first position AND B in second position (and anything after)” * [A, B] “A in first position AND B in second position (and anything after)” * [[A, B], [A, B]] “(A OR B) in first position AND (A OR B) in second position (and anything after)” */ newFilter(filter) { return this.send('eth_newFilter', filter); } /** * Creates a filter in the node, to notify when new pending transactions arrive. * * To check if the state has changed, call eth_getFilterChanges. */ newPendingTransactionFilter() { return this.send('eth_newPendingTransactionFilter'); } /** * Uninstalls a filter with given id. Should always be called when watch is no longer needed. Additonally Filters timeout when they aren’t requested with eth_getFilterChanges for a period of time. */ uninstallFilter(id) { return this.send('eth_uninstallFilter'); } /** * Returns the current ethereum protocol version. */ protocolVersion() { return this.send('eth_protocolVersion'); } /** * Returns the current ethereum protocol version. */ syncing() { return this.send('eth_syncing'); } /** * Creates new message call transaction or a contract creation for signed transactions. */ sendRawTransaction(data) { return this.send('eth_sendRawTransaction', data); } /** * signs any kind of message using the `\x19Ethereum Signed Message:\n`-prefix * @param account the address to sign the message with (if this is a 32-bytes hex-string it will be used as private key) * @param data the data to sign (Buffer, hexstring or utf8-string) */ sign(account, data) { return __awaiter(this, void 0, void 0, function* () { // prepare data const d = util_1.toBuffer(data); const hash = ethereumjs_util_1.keccak(Buffer.concat([Buffer.from('\x19Ethereum Signed Message:\n' + d.length, 'utf8'), d])); let s = { message: data, messageHash: util_1.toHex(hash) }; if (account && account.length == 66) s = Object.assign({}, s, ethereumjs_util_1.ecsign(hash, util_1.toBuffer(account))); else if (this.signer && (yield this.signer.hasAccount(account))) s = Object.assign({}, s, (yield this.signer.sign(hash, account))); s.signature = util_1.toHex(s.r) + util_1.toHex(s.s).substr(2) + util_1.toHex(s.v).substr(2); return s; }); } /** sends a Transaction */ sendTransaction(args) { return __awaiter(this, void 0, void 0, function* () { if (!args.pk && (!this.signer || !(yield this.signer.hasAccount(args.from)))) throw new Error('missing private key!'); // prepare const tx = yield prepareTransaction(args, this); let etx = null; if (args.pk) { // sign it with the raw keyx etx = new ETx(Object.assign({}, tx, { gasLimit: tx.gas })); etx.sign(util_1.toBuffer(args.pk)); } else if (this.signer && args.from) { const t = this.signer.prepareTransaction ? yield this.signer.prepareTransaction(this.client, tx) : tx; etx = new ETx(Object.assign({}, t, { gasLimit: t.gas })); const signature = yield this.signer.sign(etx.hash(false), args.from); if (etx._chainId > 0) signature.v = util_1.toHex(util_1.toNumber(signature.v) + etx._chainId * 2 + 8); Object.assign(etx, signature); } else throw new Error('Invalid transaction-data'); const txHash = yield this.sendRawTransaction(util_1.toHex(etx.serialize())); if (args.confirmations === undefined) args.confirmations = 1; // send it return args.confirmations ? confirm(txHash, this, parseInt(tx.gas), args.confirmations) : txHash; }); } contractAt(abi, address) { const api = this, ob = { _address: address, _eventHashes: {}, events: {}, _abi: abi, _in3: this.client }; for (const def of abi.filter(_ => _.type == 'function')) { const method = def.name + createSignature(def.inputs); if (def.constant) { const signature = method + ':' + createSignature(def.outputs); ob[def.name] = function (...args) { return api.callFn(address, signature, ...args) .then(r => { if (def.outputs.length > 1) { let o = {}; def.outputs.forEach((d, i) => o[i] = o[d.name] = r[i]); return o; } return r; }); }; } else { ob[def.name] = function (...args) { let tx = {}; if (args.length > def.inputs.length) tx = args.pop(); tx.method = method; tx.args = args.slice(0, def.name.length); tx.confirmations = tx.confirmations || 1; tx.to = address; return api.sendTransaction(tx); }; } ob[def.name].encode = (...args) => createCallParams(method, args.slice(0, def.name.length)).txdata; } for (const def of abi.filter(_ => _.type == 'event')) { const eHash = '0x' + ethereumjs_util_1.keccak(Buffer.from(def.name + createSignature(def.inputs), 'utf8')).toString('hex'); ob._eventHashes[def.name] = eHash; ob._eventHashes[eHash] = def; ob.events[def.name] = { getLogs(options = {}) { return api.getLogs({ address, fromBlock: options.fromBlock || 'latest', toBlock: options.toBlock || 'latest', topics: options.topics || [eHash, ...(!options.filter ? [] : def.inputs.filter(_ => _.indexed).map(d => options.filter[d.name] ? '0x' + serialize_1.bytes32(options.filter[d.name]).toString('hex') : null))], limit: options.limit || 50 }).then((logs) => logs.map(_ => { const event = ob.events.decode(_); return Object.assign({}, event, { log: _, event }); })); } }; } ob.events.decode = function (log) { return decodeEventData(log, ob); }; ob.events.all = { getLogs(options = {}) { return api.getLogs({ address, fromBlock: options.fromBlock || 'latest', toBlock: options.toBlock || 'latest', topics: options.topics || [], limit: options.limit || 50 }).then((logs) => logs.map(_ => { const event = ob.events.decode(_); return Object.assign({}, event, { log: _, event }); })); } }; return ob; } decodeEventData(log, d) { return decodeEvent(log, d); } hashMessage(data) { const d = util_1.toBuffer(data); return ethereumjs_util_1.keccak(Buffer.concat([Buffer.from('\x19Ethereum Signed Message:\n' + d.length, 'utf8'), d])); } } exports.default = API; function confirm(txHash, api, gasPaid, confirmations, timeout = 10) { return __awaiter(this, void 0, void 0, function* () { let steps = 200; const start = Date.now(); while (Date.now() - start < timeout * 1000) { const receipt = yield api.getTransactionReceipt(txHash); if (receipt) { if (!receipt.status && gasPaid && gasPaid === parseInt(receipt.gasUsed)) throw new Error('Transaction failed and all gas was used up gasPaid=' + gasPaid); if (receipt.status && receipt.status == '0x0') throw new Error('The Transaction failed because it returned status=0'); if (confirmations > 1) { const start = parseInt(receipt.blockNumber); while (start + confirmations - 1 > (yield api.blockNumber())) yield new Promise(_ => setTimeout(_, 10)); return api.getTransactionReceipt(txHash); } return receipt; } // wait a second and try again yield new Promise(_ => setTimeout(_, Math.min(timeout * 200, steps *= 2))); } throw new Error('Error waiting for the transaction to confirm'); }); } function prepareTransaction(args, api) { return __awaiter(this, void 0, void 0, function* () { const sender = args.from || (args.pk && ethereumjs_util_1.toChecksumAddress(ethereumjs_util_1.privateToAddress(util_1.toBuffer(args.pk)).toString('hex'))); const tx = {}; if (args.to) tx.to = util_1.toHex(args.to); if (args.method) { tx.data = createCallParams(args.method, args.args).txdata; if (args.data) tx.data = args.data + tx.data.substr(10); // this is the case for deploying contracts } else if (args.data) tx.data = util_1.toHex(args.data); if (sender || args.nonce) tx.nonce = util_1.toMinHex(args.nonce || (api && (yield api.getTransactionCount(sender, 'pending')))); if (api) tx.gasPrice = util_1.toMinHex(args.gasPrice || Math.round(1.3 * util_1.toNumber(yield api.gasPrice()))); tx.value = util_1.toMinHex(args.value || 0); if (sender) tx.from = sender; try { tx.gas = util_1.toMinHex(args.gas || (api && (util_1.toNumber(yield api.estimateGas(tx)) + 1000) || 3000000)); } catch (ex) { throw new Error('The Transaction ' + JSON.stringify(args, null, 2) + ' will not be succesfully executed, since estimating gas failed with: ' + ex); } return tx; }); } function convertToType(solType, v) { // check for arrays const list = solType.lastIndexOf('['); if (list >= 0) { if (!Array.isArray(v)) throw new Error('Invalid result for type ' + solType + '. Value must be an array, but is not!'); solType = solType.substr(0, list); return v.map(_ => convertToType(solType, _)); } // convert integers if (solType.startsWith('uint')) return parseInt(solType.substr(4)) <= 32 ? util_1.toNumber(v) : util_1.toBN(v); if (solType.startsWith('int')) return parseInt(solType.substr(3)) <= 32 ? util_1.toNumber(v) : util_1.toBN(v); // TODO handle negative values if (solType === 'bool') return typeof (v) === 'boolean' ? v : (util_1.toNumber(v) ? true : false); if (solType === 'string') return v.toString('utf8'); if (solType === 'address') return ethereumjs_util_1.toChecksumAddress('0x' + v); // if (solType === 'bytes') return toBuffer(v) // everything else will be hexcoded string if (Buffer.isBuffer(v)) return '0x' + v.toString('hex'); if (v && v.ixor) return '0x' + v.toString(16); return v[1] !== 'x' ? '0x' + v : v; } function decodeResult(types, result) { return ethereumjs_abi_1.rawDecode(types, result).map((v, i) => convertToType(types[i], v)); } function createCallParams(method, values) { if (!method) throw new Error('method needs to be a valid contract method signature'); if (method.indexOf('(') < 0) method += '()'; const methodRegex = /^\w+\((.*)\)$/gm; let convert = null; if (method.indexOf(':') > 0) { const srcFullMethod = method; const retTypes = method.split(':')[1].substr(1).replace(')', ' ').trim().split(','); convert = result => { if (result) result = decodeResult(retTypes, Buffer.from(result.substr(2), 'hex')); if (Array.isArray(result) && (!srcFullMethod.endsWith(')') || result.length == 1)) result = result[0]; return result; }; method = method.substr(0, method.indexOf(':')); } const m = methodRegex.exec(method); if (!m) throw new Error('No valid method signature for ' + method); const types = m[1].split(',').filter(_ => _); if (values.length < types.length) throw new Error('invalid number of arguments. Must be at least ' + types.length); values.forEach((v, i) => { if (types[i] === 'bytes') values[i] = util_1.toBuffer(v); }); return { txdata: '0x' + (values.length ? ethereumjs_abi_1.simpleEncode(method, ...values).toString('hex') : ethereumjs_abi_1.methodID(method.substr(0, method.indexOf('(')), []).toString('hex')), convert }; } function createSignatureHash(def) { return ethereumjs_util_1.keccak(def.name + createSignature(def.inputs)); } exports.createSignatureHash = createSignatureHash; function createSignature(fields) { return '(' + fields.map(f => { let baseType = f.type; const t = baseType.indexOf('['); if (t > 0) baseType = baseType.substr(0, t); if (baseType === 'uint' || baseType === 'int') baseType += '256'; return baseType + (t < 0 ? '' : f.type.substr(t)); }).join(',') + ')'; } exports.createSignature = createSignature; function parseABIString(def) { const [name, args] = def.split(/[\(\)]/); return { name, type: 'event', inputs: args.split(',').filter(_ => _).map(_ => _.split(' ').filter(z => z)).map(_ => ({ type: _[0], name: _[_.length - 1], indexed: _[1] == 'indexed' })) }; } function decodeEventData(log, def) { let d = (typeof def === 'object') ? def._eventHashes[log.topics[0]] : parseABIString(def); if (!d) return null; //throw new Error('Could not find the ABI') return decodeEvent(log, d); } function decodeEvent(log, d) { const indexed = d.inputs.filter(_ => _.indexed), unindexed = d.inputs.filter(_ => !_.indexed), r = { event: d && d.name }; if (indexed.length) decodeResult(indexed.map(_ => _.type), Buffer.concat(log.topics.slice(1).map(serialize_1.bytes))).forEach((v, i) => r[indexed[i].name] = v); if (unindexed.length) decodeResult(unindexed.map(_ => _.type), serialize_1.bytes(log.data)).forEach((v, i) => r[unindexed[i].name] = v); return r; } exports.decodeEvent = decodeEvent; class SimpleSigner { constructor(...pks) { this.accounts = {}; if (pks) pks.forEach(_ => this.addAccount(_)); } addAccount(pk) { const adr = ethereumjs_util_1.toChecksumAddress(util_1.toHex(ethereumjs_util_1.privateToAddress(util_1.toBuffer(pk)))); this.accounts[adr] = pk; return adr; } hasAccount(account) { return __awaiter(this, void 0, void 0, function* () { return !!this.accounts[ethereumjs_util_1.toChecksumAddress(account)]; }); } sign(data, account) { return __awaiter(this, void 0, void 0, function* () { const pk = util_1.toBuffer(this.accounts[ethereumjs_util_1.toChecksumAddress(account)]); if (!pk || pk.length != 32) throw new Error('Account not found for signing ' + account); const sig = ethereumjs_util_1.ecsign(data, pk); return { messageHash: util_1.toHex(data), v: util_1.toHex(sig.v), r: util_1.toHex(sig.r), s: util_1.toHex(sig.s), message: util_1.toHex(data) }; }); } } exports.SimpleSigner = SimpleSigner; function soliditySha3(...args) { return util_1.toHex(ethereumjs_util_1.keccak(ethereumjs_abi_1.rawEncode(args.map(_ => { switch (typeof (_)) { case 'number': return _ < 0 ? 'int256' : 'uint256'; case 'string': return _.substr(0, 2) === '0x' ? 'bytes' : 'string'; case 'boolean': return 'bool'; default: return BN.isBN(_) ? 'uint256' : 'bytes'; } }), args))); } exports.soliditySha3 = soliditySha3; function toHexBlock(b) { return typeof b === 'string' ? b : util_1.toMinHex(b); } //# sourceMappingURL=api.js.map