From c84df0ab04555f9460bdd958df5d4f082ce27880 Mon Sep 17 00:00:00 2001 From: Eduardo Pereira Date: Tue, 19 Mar 2019 20:03:38 -0300 Subject: [PATCH] Adaptations to be compatible with block explorers * Better docs on loom provider responses * Improved interfaces on loom provider * Do not throw when cant find getCode * Improving block 0 handling * Added strict mode that remove non standard variables --- .travis_e2e_test.sh | 2 +- src/client.ts | 12 +- src/internal/ws-rpc-client.ts | 4 +- src/loom-provider.ts | 290 ++++++++++++++++++----- src/tests/e2e/loom-provider-subscribe.ts | 137 ++++++++--- src/tests/evm-helpers.ts | 4 +- 6 files changed, 351 insertions(+), 98 deletions(-) diff --git a/.travis_e2e_test.sh b/.travis_e2e_test.sh index 88298344..247bb2a2 100755 --- a/.travis_e2e_test.sh +++ b/.travis_e2e_test.sh @@ -4,7 +4,7 @@ set -euxo pipefail eval "$(GIMME_GO_VERSION=1.10.2 gimme)" -export BUILD_ID=build-762 +export BUILD_ID=build-880 bash e2e_tests.sh diff --git a/src/client.ts b/src/client.ts index 3c29b900..df59db21 100644 --- a/src/client.ts +++ b/src/client.ts @@ -443,9 +443,12 @@ export class Client extends EventEmitter { * @param txHash Transaction hash returned by call transaction. * @return EvmTxReceipt The corresponding transaction receipt. */ - async getEvmTxReceiptAsync(txHash: Uint8Array): Promise { + async getEvmTxReceiptAsync(txHashArr: Uint8Array): Promise { + const txHash = Uint8ArrayToB64(txHashArr) + debugLog(`Get EVM receipt for ${txHash}`) + const result = await this._readClient.sendAsync('evmtxreceipt', { - txHash: Uint8ArrayToB64(txHash) + txHash }) if (result) { return EvmTxReceipt.deserializeBinary(bufferToProtobufBytes(B64ToUint8Array(result))) @@ -641,8 +644,11 @@ export class Client extends EventEmitter { hashHexStr: string, full: boolean = true ): Promise { + const hash = Buffer.from(hashHexStr.slice(2), 'hex').toString('base64') + debugLog(`Evm block by hash ${hash}`) + const result = await this._readClient.sendAsync('getevmblockbyhash', { - hash: Buffer.from(hashHexStr.slice(2), 'hex').toString('base64'), + hash, full }) if (result) { diff --git a/src/internal/ws-rpc-client.ts b/src/internal/ws-rpc-client.ts index d5653619..fc245da4 100644 --- a/src/internal/ws-rpc-client.ts +++ b/src/internal/ws-rpc-client.ts @@ -198,7 +198,7 @@ export class WSRPCClient extends EventEmitter { */ async sendAsync(method: string, params: object | any[]): Promise { await this.ensureConnectionAsync() - log(`Sending RPC msg to ${this.url}, method ${method}`) + log(`Sending RPC msg to ${this.url}, method ${method} with params ${JSON.stringify(params)}`) return this._client.call(method, params, this.requestTimeout) } @@ -217,5 +217,7 @@ export class WSRPCClient extends EventEmitter { log('EVM Event arrived', msg) this.emit(RPCClientEvent.EVMMessage, this.url, msg) } + + log('Non-Loom and Non-EVM event', msg) } } diff --git a/src/loom-provider.ts b/src/loom-provider.ts index fe51a512..b1b2526c 100644 --- a/src/loom-provider.ts +++ b/src/loom-provider.ts @@ -35,47 +35,108 @@ import { import { soliditySha3 } from './solidity-helpers' import { marshalBigUIntPB } from './big-uint' +// Based on https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt export interface IEthReceipt { transactionHash: string transactionIndex: string blockHash: string blockNumber: string - gasUsed: string + from?: string + to?: string cumulativeGasUsed: string + gasUsed: string contractAddress: string logs: Array + logsBloom?: string + root?: string status: string } +// Based on https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyhash export interface IEthTransaction { - hash: string - nonce: string blockHash: string blockNumber: string - transactionIndex: string from: string - to: string - value: string - gasPrice: string gas: string + gasPrice: string + hash: string input: string + nonce: string + to: string + transactionIndex: string + value: string + v?: string + r?: string + s?: string } +// Based on https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash export interface IEthBlock { - blockNumber: string - transactionHash: string + number: string | null + hash: string parentHash: string + nonce: string + sha3Uncles: string logsBloom: string - timestamp: number - transactions: Array + transactionsRoot: string + stateRoot: string + receiptsRoot: string + miner: string + difficulty: string + totalDifficulty: string + extraData: string + size: string + gasLimit: string + gasUsed: string + timestamp: string + transactions: Array + uncles: Array } -export interface IEthRPCPayload { - id: number - method: string - params: Array +// Based on https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB#newheads +export interface IEthPubSubNewHeads { + jsonrpc: '2.0' + method: 'eth_subscription' + params: { + subscription: string + result: { + difficulty: string + extraData: string + gasLimit: string + gasUsed: string + logsBloom: string + miner: string + nonce: string + number: string + parentHash: string + receiptRoot: string + sha3Uncles: string + timestamp: string + transactionsRoot: string + } + } +} + +// Based on https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB#logs +export interface IEthPubLogs { + jsonrpc: '2.0' + method: 'eth_subscription' + params: { + subscription: string + result: { + address: string + blockHash: string + blockNumber: string + data: string + logIndex: string + topics: Array + transactionHash: string + transactionIndex: string + } + } } +// Based on https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges export interface IEthFilterLog { removed: boolean logIndex: string @@ -86,6 +147,15 @@ export interface IEthFilterLog { address: string data: string topics: Array + // blockTime is an addition isn't part of the specification + blockTime: string +} + +// JSON RPC payload +export interface IEthRPCPayload { + id: number + method: string + params: Array } export type SetupMiddlewareFunction = ( @@ -106,6 +176,35 @@ const numberToHexLC = (num: number): string => { return numberToHex(num).toLowerCase() } +const ZEROED_HEX_256 = + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +const ZEROED_HEX_32 = '0x0000000000000000000000000000000000000000000000000000000000000000' +const ZEROED_HEX_20 = '0x0000000000000000000000000000000000000000' +const ZEROED_HEX_8 = '0x0000000000000000' +const ZEROED_HEX = '0x0' + +const BLOCK_ZERO: IEthBlock = { + number: ZEROED_HEX, + hash: '0x0000000000000000000000000000000000000000000000000000000000000001', + parentHash: ZEROED_HEX_32, + nonce: ZEROED_HEX_8, + sha3Uncles: ZEROED_HEX_32, + logsBloom: ZEROED_HEX_256, + transactionsRoot: ZEROED_HEX_32, + stateRoot: ZEROED_HEX_32, + receiptsRoot: ZEROED_HEX_32, + miner: ZEROED_HEX_20, + difficulty: ZEROED_HEX, + totalDifficulty: ZEROED_HEX, + extraData: ZEROED_HEX, + size: ZEROED_HEX, + gasLimit: ZEROED_HEX, + gasUsed: ZEROED_HEX, + timestamp: '0x5af97a40', + transactions: [], + uncles: [] +} + /** * Web3 provider that interacts with EVM contracts deployed on Loom DAppChains. */ @@ -116,6 +215,7 @@ export class LoomProvider { private _setupMiddlewares: SetupMiddlewareFunction private _netVersionFromChainId: number private _ethRPCMethods: Map + private _strict: boolean = false protected notificationCallbacks: Array readonly accounts: Map @@ -187,6 +287,14 @@ export class LoomProvider { }) } + get strict() { + return this._strict + } + + set strict(v: boolean) { + this._strict = v + } + // PUBLIC FUNCTION TO SUPPORT WEB3 on(type: string, callback: any) { @@ -418,6 +526,10 @@ export class LoomProvider { const blockHash = payload.params[0] const isFull = payload.params[1] || true + if (blockHash === ZEROED_HEX_32) { + return Promise.resolve(BLOCK_ZERO) + } + const result = await this._client.getEvmBlockByHashAsync(blockHash, isFull) if (!result) { @@ -431,6 +543,11 @@ export class LoomProvider { const blockNumberToSearch = payload.params[0] === 'latest' ? payload.params[0] : hexToNumber(payload.params[0]) const isFull = payload.params[1] || true + + if (blockNumberToSearch === 0) { + return Promise.resolve(BLOCK_ZERO) + } + const result = await this._client.getEvmBlockByNumberAsync(`${blockNumberToSearch}`, isFull) if (!result) { @@ -445,13 +562,9 @@ export class LoomProvider { this._client.chainId, LocalAddress.fromHexString(payload.params[0]) ) - const result = await this._client.getEvmCodeAsync(address) - if (!result) { - throw Error('No code returned on eth_getCode') - } - - return bytesToHexAddrLC(result) + const result = await this._client.getEvmCodeAsync(address) + return result ? bytesToHexAddrLC(result) : '0x0' } private async _ethGetFilterChanges(payload: IEthRPCPayload) { @@ -513,6 +626,7 @@ export class LoomProvider { }) }) }) + return this._createReceiptResult(receipt!) } @@ -672,12 +786,13 @@ export class LoomProvider { return this._client.queryAsync(address, data, VMType.EVM, caller) } - private _createBlockInfo(blockInfo: EthBlockInfo, isFull: boolean): any { - const blockNumber = numberToHexLC(blockInfo.getNumber()) - const transactionHash = bytesToHexAddrLC(blockInfo.getHash_asU8()) - const parentHash = bytesToHexAddrLC(blockInfo.getParentHash_asU8()) + private _createBlockInfo(blockInfo: EthBlockInfo, isFull: boolean): IEthBlock { + // tslint:disable-next-line:variable-name + const number = numberToHexLC(blockInfo.getNumber()) + const hash = bytesToHexAddrLC(blockInfo.getHash_asU8()) + let parentHash = bytesToHexAddrLC(blockInfo.getParentHash_asU8()) const logsBloom = bytesToHexAddrLC(blockInfo.getLogsBloom_asU8()) - const timestamp = blockInfo.getTimestamp() + const timestamp = numberToHexLC(blockInfo.getTimestamp()) const transactions = blockInfo.getTransactionsList_asU8().map((transaction: Uint8Array) => { if (isFull) { return this._createTransactionResult( @@ -688,18 +803,33 @@ export class LoomProvider { } }) + // Parent hash is empty for the block 0x1 so this fix it + if (parentHash === '0x' && number === '0x1') { + parentHash = '0x0000000000000000000000000000000000000000000000000000000000000001' + } + + // Some ZEROED values aren't at the moment return { - blockNumber, - transactionHash, + number, + hash, parentHash, + nonce: ZEROED_HEX_8, + sha3Uncles: ZEROED_HEX_32, logsBloom, + transactionsRoot: ZEROED_HEX_32, + stateRoot: ZEROED_HEX_32, + receiptsRoot: ZEROED_HEX_32, + miner: ZEROED_HEX_20, + difficulty: ZEROED_HEX, + totalDifficulty: ZEROED_HEX, + extraData: ZEROED_HEX, + size: ZEROED_HEX, + gasLimit: ZEROED_HEX, + gasUsed: ZEROED_HEX, timestamp, transactions, - gasLimit: '0x0', - gasUsed: '0x0', - size: '0x0', - number: '0x0' - } + uncles: [] + } as IEthBlock } private _createTransactionResult(txObject: EvmTxObject): IEthTransaction { @@ -710,23 +840,27 @@ export class LoomProvider { const transactionIndex = numberToHexLC(txObject.getTransactionIndex()) const from = bytesToHexAddrLC(txObject.getFrom_asU8()) const to = bytesToHexAddrLC(txObject.getTo_asU8()) - const value = `${txObject.getValue()}` + const value = numberToHexLC(txObject.getValue()) const gas = numberToHexLC(txObject.getGas()) const gasPrice = numberToHexLC(txObject.getGasPrice()) - const input = bytesToHexAddrLC(txObject.getInput_asU8()) + let input = bytesToHexAddrLC(txObject.getInput_asU8()) + + if (input === '0x') { + input = '0x0' + } return { - hash, - nonce, blockHash, blockNumber, - transactionIndex, from, - to, - value, gas, gasPrice, - input + hash, + input, + nonce, + to, + transactionIndex, + value } as IEthTransaction } @@ -735,39 +869,51 @@ export class LoomProvider { const transactionIndex = numberToHexLC(receipt.getTransactionIndex()) const blockHash = bytesToHexAddrLC(receipt.getBlockHash_asU8()) const blockNumber = numberToHexLC(receipt.getBlockNumber()) + const cumulativeGasUsed = numberToHexLC(receipt.getCumulativeGasUsed()) + const gasUsed = numberToHexLC(receipt.getGasUsed()) const contractAddress = bytesToHexAddrLC(receipt.getContractAddress_asU8()) const logs = receipt.getLogsList().map((logEvent: EventData, index: number) => { const logIndex = numberToHexLC(index) let data = bytesToHexAddrLC(logEvent.getEncodedBody_asU8()) if (data === '0x') { - data = '0x0' + data = ZEROED_HEX_32 } - return { + const blockHash = bytesToHexAddrLC(receipt.getBlockHash_asU8()) + + const log = { logIndex, address: contractAddress, blockHash, blockNumber, - blockTime: logEvent.getBlockTime(), transactionHash: bytesToHexAddrLC(logEvent.getTxHash_asU8()), transactionIndex, type: 'mined', data, topics: logEvent.getTopicsList().map((topic: string) => topic.toLowerCase()) } + + return this._strict ? log : Object.assign({}, log, { blockTime: logEvent.getBlockTime() }) }) + const status = numberToHexLC(receipt.getStatus()) + + // Commented properties aren't supported at the current version return { transactionHash, transactionIndex, blockHash, blockNumber, + // from, + // to, + cumulativeGasUsed, + gasUsed, contractAddress, - gasUsed: numberToHexLC(receipt.getGasUsed()), - cumulativeGasUsed: numberToHexLC(receipt.getCumulativeGasUsed()), logs, - status: numberToHexLC(receipt.getStatus()) + // logsBloom, + // root, + status } as IEthReceipt } @@ -806,17 +952,28 @@ export class LoomProvider { } private _createLogResult(log: EthFilterLog): IEthFilterLog { + const removed = log.getRemoved() + const blockTime = numberToHexLC(log.getBlockTime()) + const logIndex = numberToHexLC(log.getLogIndex()) + const transactionIndex = numberToHex(log.getTransactionIndex()) + const transactionHash = bytesToHexAddrLC(log.getTransactionHash_asU8()) + const blockHash = bytesToHexAddrLC(log.getBlockHash_asU8()) + const blockNumber = numberToHex(log.getBlockNumber()) + const address = bytesToHexAddrLC(log.getAddress_asU8()) + const data = bytesToHexAddrLC(log.getData_asU8()) + const topics = log.getTopicsList().map((topic: any) => String.fromCharCode.apply(null, topic)) + return { - removed: log.getRemoved(), - blockTime: log.getBlockTime(), - logIndex: numberToHexLC(log.getLogIndex()), - transactionIndex: numberToHex(log.getTransactionIndex()), - transactionHash: bytesToHexAddrLC(log.getTransactionHash_asU8()), - blockHash: bytesToHexAddrLC(log.getBlockHash_asU8()), - blockNumber: numberToHex(log.getBlockNumber()), - address: bytesToHexAddrLC(log.getAddress_asU8()), - data: bytesToHexAddrLC(log.getData_asU8()), - topics: log.getTopicsList().map((topic: any) => String.fromCharCode.apply(null, topic)) + removed, + logIndex, + transactionIndex, + transactionHash, + blockHash, + blockNumber, + address, + data, + topics, + blockTime } as IEthFilterLog } @@ -837,24 +994,29 @@ export class LoomProvider { if (msgEvent.kind === ClientEvent.EVMEvent) { log(`Socket message arrived ${JSON.stringify(msgEvent)}`) this.notificationCallbacks.forEach((callback: Function) => { + const transactionHash = bytesToHexAddrLC(msgEvent.transactionHashBytes) + const blockNumber = numberToHexLC(+msgEvent.blockHeight) + const data = bytesToHexAddrLC(msgEvent.data) + + // Some 0x0 values aren't at the moment const JSONRPCResult = { jsonrpc: '2.0', method: 'eth_subscription', params: { subscription: msgEvent.id, result: { - transactionHash: bytesToHexAddrLC(msgEvent.transactionHashBytes), - logIndex: '0x0', - transactionIndex: '0x0', - blockHash: '0x0', - blockNumber: numberToHexLC(+msgEvent.blockHeight), + transactionHash, + logIndex: ZEROED_HEX, + transactionIndex: ZEROED_HEX, + blockHash: ZEROED_HEX, + blockNumber, address: msgEvent.contractAddress.local.toString(), type: 'mined', - data: bytesToHexAddrLC(msgEvent.data), + data, topics: msgEvent.topics } } - } + } as IEthPubLogs | IEthPubSubNewHeads callback(JSONRPCResult) }) diff --git a/src/tests/e2e/loom-provider-subscribe.ts b/src/tests/e2e/loom-provider-subscribe.ts index abb412ed..64340ebe 100644 --- a/src/tests/e2e/loom-provider-subscribe.ts +++ b/src/tests/e2e/loom-provider-subscribe.ts @@ -1,36 +1,15 @@ import test from 'tape' import { LocalAddress, CryptoUtils } from '../../index' -import { createTestClient, execAndWaitForMillisecondsAsync } from '../helpers' +import { + createTestClient, + execAndWaitForMillisecondsAsync, + waitForMillisecondsAsync +} from '../helpers' import { LoomProvider } from '../../loom-provider' import { deployContract } from '../evm-helpers' -/** - * Requires the SimpleStore solidity contract deployed on a loomchain. - * go-loom/examples/plugins/evmexample/contract/SimpleStore.sol - * - * pragma solidity ^0.4.22; - * - * contract SimpleStore { - * uint value; - * - * constructor() { - * value = 10; - * } - * - * event NewValueSet(uint _value); - * - * function set(uint _value) public { - * value = _value; - * emit NewValueSet(value); - * } - * - * function get() public view returns (uint) { - * return value; - * } - * } - * - */ +const Web3 = require('web3') const contractData = '608060405234801561001057600080fd5b50600a600081905550610114806100286000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c14606c575b600080fd5b606a600480360381019080803590602001909291905050506094565b005b348015607757600080fd5b50607e60df565b6040518082815260200191505060405180910390f35b806000819055507f2afa03c814297ffc234ff967b6f0863d3c358be243103f20217c8d3a4d39f9c060005434604051808381526020018281526020019250505060405180910390a150565b600080549050905600a165627a7a72305820deed812a797567167162d0af3ae5f0528c39bea0620e32b28e243628cd655dc40029' @@ -108,3 +87,107 @@ test('LoomProvider + Subscribe', async t => { t.end() }) + +test('LoomProvider + BlockByNumber transaction issue', async t => { + let client + + try { + const privKey = CryptoUtils.generatePrivateKey() + client = createTestClient() + const from = LocalAddress.fromPublicKey( + CryptoUtils.publicKeyFromPrivateKey(privKey) + ).toString() + client.on('error', msg => console.error('error', msg)) + const loomProvider = new LoomProvider(client, privKey) + + const contractData = + '0x608060405234801561001057600080fd5b50600a60008190555061010e806100286000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60d9565b6040518082815260200191505060405180910390f35b806000819055506000547fb922f092a64f1a076de6f21e4d7c6400b6e55791cc935e7bb8e7e90f7652f15b60405160405180910390a250565b600080549050905600a165627a7a72305820b76f6c855a1f95260fc70490b16774074225da52ea165a58e95eb7a72a59d1700029' + + const ABI = [ + { + constant: false, + inputs: [{ name: '_value', type: 'uint256' }], + name: 'set', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'get', + outputs: [{ name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { inputs: [], payable: false, stateMutability: 'nonpayable', type: 'constructor' }, + { + anonymous: false, + inputs: [{ indexed: true, name: '_value', type: 'uint256' }], + name: 'NewValueSet', + type: 'event' + } + ] + + const result = await deployContract(loomProvider, contractData) + + const web3 = new Web3(loomProvider) + const contract = new web3.eth.Contract(ABI, result.contractAddress, { from }) + + // Just call a contract to call an event on loomchain + const tx = await contract.methods.set(1).send({ from }) + + // Subscribe for new heads using eth_subscribe + const ethGetBlockByNumberResult = await execAndWaitForMillisecondsAsync( + loomProvider.sendAsync({ + id: 1000, + method: 'eth_getBlockByNumber', + params: [`0x${tx.blockNumber.toString(16)}`] + }) + ) + + t.equal( + ethGetBlockByNumberResult.result.number, + `0x${tx.blockNumber.toString(16)}`, + 'Should block returned by the number match' + ) + + t.assert( + ethGetBlockByNumberResult.result.transactions.length > 0, + 'Should exists transactions on block' + ) + + // Subscribe for new heads using eth_subscribe + const ehtGetBlockByHashResult = await execAndWaitForMillisecondsAsync( + loomProvider.sendAsync({ + id: 1000, + method: 'eth_getBlockByHash', + params: [tx.blockHash] + }) + ) + + t.equal( + ehtGetBlockByHashResult.result.hash, + tx.blockHash, + 'Should block returned by the hash match' + ) + + t.assert( + ehtGetBlockByHashResult.result.transactions.length > 0, + 'Should exists transactions on block' + ) + + // Waiting for newHeads events appear on log + await waitForMillisecondsAsync(5000) + } catch (err) { + console.log(err) + } + + if (client) { + client.disconnect() + } + + t.end() +}) diff --git a/src/tests/evm-helpers.ts b/src/tests/evm-helpers.ts index d58a3e34..3a401c75 100644 --- a/src/tests/evm-helpers.ts +++ b/src/tests/evm-helpers.ts @@ -14,7 +14,7 @@ export async function deployContract(loomProvider: LoomProvider, contractData: s ).toString() const ethSendTransactionDeployResult = await loomProvider.sendAsync({ - id: 1, + id: 100, method: 'eth_sendTransaction', params: [ { @@ -28,7 +28,7 @@ export async function deployContract(loomProvider: LoomProvider, contractData: s }) const ethGetTransactionReceiptResult = await loomProvider.sendAsync({ - id: 2, + id: 200, method: 'eth_getTransactionReceipt', params: [ethSendTransactionDeployResult.result] })