incubed
Version:
Typescript-version of the incubed client
576 lines • 24.9 kB
JavaScript
"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