diff --git a/.travis_e2e_test.sh b/.travis_e2e_test.sh index f3b12f5c..4cebbbf2 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-1213 +export BUILD_ID=build-1283 bash e2e_tests.sh diff --git a/e2e_support/genesis.json b/e2e_support/genesis.json index 51eeb2af..f457c767 100644 --- a/e2e_support/genesis.json +++ b/e2e_support/genesis.json @@ -1,11 +1,11 @@ { "contracts": [ - { - "vm": "plugin", - "format": "plugin", - "name": "BluePrint", - "location": "blueprint:0.0.1", - "init": {} + { + "vm": "plugin", + "format": "plugin", + "name": "BluePrint", + "location": "blueprint:0.0.1", + "init": {} }, { "vm": "plugin", @@ -62,6 +62,22 @@ { "name": "tg:binance-cm", "status": "WAITING" + }, + { + "name": "receipts:v3", + "status": "WAITING" + }, + { + "name": "receipts:v3.1", + "status": "WAITING" + }, + { + "name": "receipts:v3.2", + "status": "WAITING" + }, + { + "name": "evm:constantinople", + "status": "WAITING" } ] } @@ -99,4 +115,4 @@ } } ] -} \ No newline at end of file +} diff --git a/e2e_tests.sh b/e2e_tests.sh index 1070fe1f..4c4e84f5 100755 --- a/e2e_tests.sh +++ b/e2e_tests.sh @@ -28,7 +28,7 @@ fi download_dappchain() { cd $LOOM_DIR - wget -O loom https://private.delegatecall.com/loom/$PLATFORM/$BUILD_ID/loom-gateway + wget -O loom https://downloads.loomx.io/loom/$PLATFORM/$BUILD_ID/loom-gateway chmod +x loom LOOM_BIN=`pwd`/loom } diff --git a/src/client.ts b/src/client.ts index 9655ed8b..9eb66725 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,7 +21,7 @@ import { } from './crypto-utils' import { Address, LocalAddress } from './address' import { WSRPCClient, IJSONRPCEvent } from './internal/ws-rpc-client' -import { RPCClientEvent, IJSONRPCClient } from './internal/json-rpc-client' +import { RPCClientEvent, IJSONRPCClient, IJSONRPCResponse } from './internal/json-rpc-client' import { sleep, parseUrl } from './helpers' export interface ITxHandlerResult { @@ -243,6 +243,13 @@ export class Client extends EventEmitter { private _writeClient: IJSONRPCClient private _readClient!: IJSONRPCClient + /** + * Indicates whether the client is configured to use the /eth endpoint on Loom nodes. + * When this is enabled the client can only be used to interact with EVM contracts. + * NOTE: This limitation will be removed in the near future. + */ + readonly isWeb3EndpointEnabled: boolean + /** Broadcaster to use to send txs & receive results. */ txBroadcaster: ITxBroadcaster @@ -315,6 +322,8 @@ export class Client extends EventEmitter { ) } + this.isWeb3EndpointEnabled = true === /eth$/.test(this._readClient.url) + const emitContractEvent = (url: string, event: IJSONRPCEvent) => this._emitContractEvent(url, event) @@ -442,6 +451,17 @@ export class Client extends EventEmitter { } } + /** + * Sends a Web3 JSON-RPC message to the /eth endpoint on a Loom node. + * @param method RPC method name. + * @param params Parameter object or array. + * @returns A promise that will be resolved with the value of the result field (if any) in the + * JSON-RPC response message. + */ + async sendWeb3MsgAsync(method: string, params: object | any[]): Promise> { + return this._readClient.sendAsync>(method, params) + } + /** * Queries the receipt corresponding to a transaction hash * @@ -555,7 +575,6 @@ export class Client extends EventEmitter { const envelope: EthFilterEnvelope = EthFilterEnvelope.deserializeBinary( bufferToProtobufBytes(result) ) - switch (envelope.getMessageCase()) { case EthFilterEnvelope.MessageCase.ETH_BLOCK_HASH_LIST: return envelope.getEthBlockHashList() as EthBlockHashList @@ -675,8 +694,12 @@ export class Client extends EventEmitter { * @param method Method selected to the filter, can be "newHeads" or "logs" * @param filter JSON string of the filter */ - evmSubscribeAsync(method: string, filterObject: Object): Promise { - const filter = JSON.stringify(filterObject) + evmSubscribeAsync(method: string, params: Object | any[]): Promise { + if (this.isWeb3EndpointEnabled) { + return this._readClient.sendAsync('eth_subscribe', params) + } + + const filter = JSON.stringify(params) return this._readClient.sendAsync('evmsubscribe', { method, filter @@ -691,9 +714,11 @@ export class Client extends EventEmitter { * @return boolean If true the subscription is removed with success */ evmUnsubscribeAsync(id: string): Promise { - return this._readClient.sendAsync('evmunsubscribe', { - id - }) + if (this.isWeb3EndpointEnabled) { + return this._readClient.sendAsync('eth_unsubscribe', [id]) + } + + return this._readClient.sendAsync('evmunsubscribe', { id }) } /** @@ -748,13 +773,14 @@ export class Client extends EventEmitter { } private _emitContractEvent(url: string, event: IJSONRPCEvent, isEVM: boolean = false): void { + debugLog('_emitContractEvent', arguments) const { error, result } = event if (error) { const eventArgs: IClientErrorEventArgs = { kind: ClientEvent.Error, url, error } this.emit(ClientEvent.Error, eventArgs) + } else if (this.isWeb3EndpointEnabled && isEVM) { + this.emit(ClientEvent.EVMEvent, event) } else if (result) { - debugLog('Event', event.id, result) - // Ugh, no built-in JSON->Protobuf marshaller apparently // https://github.com/google/protobuf/issues/1591 so gotta do this manually const eventArgs: IChainEventArgs = { @@ -776,8 +802,11 @@ export class Client extends EventEmitter { transactionHashBytes: result.tx_hash ? B64ToUint8Array(result.tx_hash) : new Uint8Array([]) } - if (isEVM) this.emit(ClientEvent.EVMEvent, eventArgs) - else this.emit(ClientEvent.Contract, eventArgs) + if (isEVM) { + this.emit(ClientEvent.EVMEvent, eventArgs) + } else { + this.emit(ClientEvent.Contract, eventArgs) + } } } diff --git a/src/helpers.ts b/src/helpers.ts index 9e19adbe..3ca702d4 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -127,11 +127,16 @@ export function parseUrl(rawUrl: string): URL { } export function setupProtocolsFromEndpoint( - endpoint: string + endpoint: string, + useWeb3Endpoint: boolean = false ): { writer: IJSONRPCClient; reader: IJSONRPCClient } { const protocol = selectProtocol(endpoint) const writerSuffix = protocol === JSONRPCProtocol.HTTP ? '/rpc' : '/websocket' - const readerSuffix = protocol === JSONRPCProtocol.HTTP ? '/query' : '/queryws' + const readerSuffix = useWeb3Endpoint + ? '/eth' + : protocol === JSONRPCProtocol.HTTP + ? '/query' + : '/queryws' const writer = createJSONRPCClient({ protocols: [{ url: endpoint + writerSuffix }] diff --git a/src/internal/dual-rpc-client.ts b/src/internal/dual-rpc-client.ts index b016d54e..93258cfd 100644 --- a/src/internal/dual-rpc-client.ts +++ b/src/internal/dual-rpc-client.ts @@ -25,7 +25,7 @@ export class DualRPCClient extends WSRPCClient { requestTimeout?: number reconnectInterval?: number maxReconnects?: number - generateRequestId?: (method: string, params: object | any[]) => string + generateRequestId?: (method: string, params: object | any[]) => string | number }) { super(opts.wsUrl, opts) const { diff --git a/src/internal/http-rpc-client.ts b/src/internal/http-rpc-client.ts index 378d1f93..482723b6 100644 --- a/src/internal/http-rpc-client.ts +++ b/src/internal/http-rpc-client.ts @@ -30,7 +30,7 @@ export class HTTPRPCClient extends EventEmitter implements IJSONRPCClient { public url: string, opts: { requestTimeout?: number - generateRequestId?: (method: string, params: object | any[]) => string + generateRequestId?: (method: string, params: object | any[]) => string | number } = {} ) { super() @@ -40,6 +40,7 @@ export class HTTPRPCClient extends EventEmitter implements IJSONRPCClient { } = opts this.requestTimeout = requestTimeout + // FIXME: generateRequestId doesn't actually override this._getNextRequestId at the moment } disconnect() { diff --git a/src/internal/json-rpc-client.ts b/src/internal/json-rpc-client.ts index cbaec5c8..3916f528 100644 --- a/src/internal/json-rpc-client.ts +++ b/src/internal/json-rpc-client.ts @@ -10,19 +10,19 @@ export interface IJSONRPCRequest { jsonrpc: '2.0' method: string params?: any - id?: string + id?: string | number } export interface IJSONRPCResultResponse { jsonrpc: '2.0' result: T - id: string + id: string | number } export interface IJSONRPCErrorResponse { jsonrpc: '2.0' error: IJSONRPCError - id: string + id: string | number } export interface IJSONRPCResponse extends IJSONRPCResultResponse, IJSONRPCErrorResponse {} diff --git a/src/internal/ws-rpc-client.ts b/src/internal/ws-rpc-client.ts index d5653619..8eb4be86 100644 --- a/src/internal/ws-rpc-client.ts +++ b/src/internal/ws-rpc-client.ts @@ -35,7 +35,11 @@ export class WSRPCClient extends EventEmitter { private _isSubcribed: boolean = false protected _rpcId: number = 0 - protected _getNextRequestId = () => (++this._rpcId).toString() + + protected _getNextRequestId = () => { + const id = ++this._rpcId + return this.isWeb3EndpointEnabled ? id.toString() : id + } requestTimeout: number @@ -43,6 +47,13 @@ export class WSRPCClient extends EventEmitter { return this._isSubcribed } + /** + * Indicates whether the client is configured to use the /eth endpoint on Loom nodes. + * When this is enabled the client can only be used to interact with EVM contracts. + * NOTE: This limitation will be removed in the near future. + */ + readonly isWeb3EndpointEnabled: boolean + /** * * @param url @@ -59,10 +70,13 @@ export class WSRPCClient extends EventEmitter { requestTimeout?: number reconnectInterval?: number maxReconnects?: number - generateRequestId?: (method: string, params: object | any[]) => string + generateRequestId?: (method: string, params: object | any[]) => string | number } = {} ) { super() + + this.isWeb3EndpointEnabled = true === /eth$/.test(url) + const { autoConnect = true, requestTimeout = 15000, // 15s @@ -206,15 +220,15 @@ export class WSRPCClient extends EventEmitter { const msgStr = message instanceof ArrayBuffer ? Buffer.from(message).toString() : message const msg = JSON.parse(msgStr) - // Events from native loomchain have the id equals 0 - if (msg.id === '0') { - log('Loom Event arrived', msg) + if (msg.method === 'eth_subscription') { + log('EVM contract event arrived', msg) + this.emit(RPCClientEvent.EVMMessage, this.url, msg) + } else if (msg.id === '0') { + // Events from native loomchain have id 0 + log('Go contract event arrived', msg) this.emit(RPCClientEvent.Message, this.url, msg) - } - - // Events from EVM have the id from the evmsubscribe command - if (/^0x.+$/.test(msg.id)) { - log('EVM Event arrived', msg) + } else if (/^0x.+$/.test(msg.id)) { + // Events from EVM have the id from the evmsubscribe command this.emit(RPCClientEvent.EVMMessage, this.url, msg) } } diff --git a/src/loom-provider.ts b/src/loom-provider.ts index fd8c764f..3a711326 100644 --- a/src/loom-provider.ts +++ b/src/loom-provider.ts @@ -34,7 +34,7 @@ import { } from './crypto-utils' import { soliditySha3 } from './solidity-helpers' import { marshalBigUIntPB } from './big-uint' -import { SignedEthTxMiddleware } from './middleware' +import { IJSONRPCEvent } from './internal/ws-rpc-client' export interface IEthReceipt { transactionHash: string @@ -113,11 +113,14 @@ const numberToHexLC = (num: number): string => { export class LoomProvider { private _client: Client private _subscribed: boolean = false + private _ethSubscriptions: { [id: string]: any } = {} private _accountMiddlewares: Map> private _setupMiddlewares: SetupMiddlewareFunction private _netVersionFromChainId: number private _ethRPCMethods: Map + protected notificationCallbacks: Array + readonly accounts: Map /** @@ -159,18 +162,22 @@ export class LoomProvider { this.accounts = new Map() // Only subscribe for event emitter do not call subevents - this._client.addListener(ClientEvent.EVMEvent, (msg: IChainEventArgs) => + this._client.addListener(ClientEvent.EVMEvent, (msg: IChainEventArgs | IJSONRPCEvent) => this._onWebSocketMessage(msg) ) if (!this._setupMiddlewares) { this._setupMiddlewares = (client: Client, privateKey: Uint8Array) => { - return createDefaultTxMiddleware(client, privateKey as Uint8Array) + return createDefaultTxMiddleware(client, privateKey) } } + if (client.isWeb3EndpointEnabled) { + this._addDefaultMethods() + } else { + this._addLegacyDefaultMethods() + } - this.addDefaultMethods() - this.addDefaultEvents() + this._addDefaultEvents() this.addAccounts([privateKey]) } @@ -212,6 +219,7 @@ export class LoomProvider { // PUBLIC FUNCTION TO SUPPORT WEB3 on(type: string, callback: any) { + log('on', type) switch (type) { case 'data': this.notificationCallbacks.push(callback) @@ -228,14 +236,50 @@ export class LoomProvider { } } - addDefaultEvents() { + private _addDefaultEvents() { this._client.addListener(ClientEvent.Disconnected, () => { // reset all requests and callbacks - this.reset() + this.notificationCallbacks = [] }) } - addDefaultMethods() { + /** + * Sets up the provider to interact with the Loom /eth endpoint. + * This endpoint emulates the Web3 JSON-RPC API so most messages don't require transformation. + */ + private _addDefaultMethods() { + this._ethRPCMethods.set('eth_accounts', this._ethAccounts) + this._ethRPCMethods.set('eth_blockNumber', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_call', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_estimateGas', this._ethEstimateGas) + this._ethRPCMethods.set('eth_gasPrice', this._ethGasPrice) + this._ethRPCMethods.set('eth_getBalance', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getBlockByHash', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getBlockByNumber', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getCode', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getFilterChanges', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getLogs', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getTransactionByHash', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_getTransactionReceipt', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_newBlockFilter', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_newFilter', this._ethCallSupportedMethod) + this._ethRPCMethods.set( + 'eth_newPendingTransactionFilter', + this._ethNewPendingTransactionFilter + ) + this._ethRPCMethods.set('eth_sendTransaction', this._ethSendTransaction) + this._ethRPCMethods.set('eth_sign', this._ethSign) + this._ethRPCMethods.set('eth_subscribe', this._ethSubscribe) + this._ethRPCMethods.set('eth_uninstallFilter', this._ethCallSupportedMethod) + this._ethRPCMethods.set('eth_unsubscribe', this._ethUnsubscribe) + this._ethRPCMethods.set('net_version', this._netVersion) + } + + /** + * Sets up the provider to interact with the Loom /query endpoint, which requires Web3 JSON-RPC + * messages to be marshalled to protobufs. + */ + private _addLegacyDefaultMethods() { this._ethRPCMethods.set('eth_accounts', this._ethAccounts) this._ethRPCMethods.set('eth_blockNumber', this._ethBlockNumber) this._ethRPCMethods.set('eth_call', this._ethCall) @@ -257,9 +301,9 @@ export class LoomProvider { ) this._ethRPCMethods.set('eth_sendTransaction', this._ethSendTransaction) this._ethRPCMethods.set('eth_sign', this._ethSign) - this._ethRPCMethods.set('eth_subscribe', this._ethSubscribe) + this._ethRPCMethods.set('eth_subscribe', this._ethSubscribeLegacy) this._ethRPCMethods.set('eth_uninstallFilter', this._ethUninstallFilter) - this._ethRPCMethods.set('eth_unsubscribe', this._ethUnsubscribe) + this._ethRPCMethods.set('eth_unsubscribe', this._ethUnsubscribeLegacy) this._ethRPCMethods.set('net_version', this._netVersion) } @@ -335,10 +379,6 @@ export class LoomProvider { } } - reset() { - this.notificationCallbacks = [] - } - disconnect() { this._client.disconnect() } @@ -393,6 +433,10 @@ export class LoomProvider { // PRIVATE FUNCTIONS EVM CALLS + private _ethCallSupportedMethod(payload: IEthRPCPayload): Promise { + return this._client.sendWeb3MsgAsync(payload.method, payload.params) + } + private _ethAccounts() { if (this.accounts.size === 0) { throw Error('No account available') @@ -484,17 +528,11 @@ export class LoomProvider { } if (result instanceof EthBlockHashList) { - return (result as EthBlockHashList) - .getEthBlockHashList_asU8() - .map((hash: Uint8Array) => bytesToHexAddrLC(hash)) + return result.getEthBlockHashList_asU8().map((hash: Uint8Array) => bytesToHexAddrLC(hash)) } else if (result instanceof EthTxHashList) { - return (result as EthTxHashList) - .getEthTxHashList_asU8() - .map((hash: Uint8Array) => bytesToHexAddrLC(hash)) + return result.getEthTxHashList_asU8().map((hash: Uint8Array) => bytesToHexAddrLC(hash)) } else if (result instanceof EthFilterLogList) { - return (result as EthFilterLogList) - .getEthBlockLogsList() - .map((log: EthFilterLog) => this._createLogResult(log)) + return result.getEthBlockLogsList().map((log: EthFilterLog) => this._createLogResult(log)) } } @@ -596,10 +634,10 @@ export class LoomProvider { return bytesToHexAddrLC(Buffer.concat([sig.r, sig.s, toBuffer(sig.v)])) } - private async _ethSubscribe(payload: IEthRPCPayload) { + private async _ethSubscribeLegacy(payload: IEthRPCPayload) { if (!this._subscribed) { this._subscribed = true - this._client.addListenerForTopics() + this._client.addListenerForTopics().catch(console.error) } const method = payload.params[0] @@ -613,14 +651,46 @@ export class LoomProvider { return result } + private async _ethSubscribe(payload: IEthRPCPayload) { + if (!this._subscribed) { + this._subscribed = true + this._client.addListenerForTopics().catch(console.error) + } + const { method, params } = payload + const subscriptionId: string = await this._client.evmSubscribeAsync(method, params) + log('subscribed', subscriptionId, params) + this._ethSubscriptions[subscriptionId] = { + id: subscriptionId, + method, + params + } + + return subscriptionId + } + private _ethUninstallFilter(payload: IEthRPCPayload) { return this._client.uninstallEvmFilterAsync(payload.params[0]) } - private _ethUnsubscribe(payload: IEthRPCPayload) { + private _ethUnsubscribeLegacy(payload: IEthRPCPayload) { return this._client.evmUnsubscribeAsync(payload.params[0]) } + private async _ethUnsubscribe(payload: IEthRPCPayload) { + const subscriptionId = payload.params[0] + const unsubscribeMethod = payload.params[1] || 'eth_unsubscribe' + const subscription = this._ethSubscriptions[subscriptionId] + if (subscription !== undefined) { + const response = await this._client.sendWeb3MsgAsync(unsubscribeMethod, [subscriptionId]) + if (response) { + delete this._ethSubscriptions[subscriptionId] + } + return response + } + + throw new Error(`Provider error: Subscription with ID ${subscriptionId} does not exist.`) + } + private _netVersion() { return this._netVersionFromChainId } @@ -716,6 +786,7 @@ export class LoomProvider { return { blockNumber, + number: blockNumber, transactionHash, parentHash, logsBloom, @@ -723,8 +794,7 @@ export class LoomProvider { transactions, gasLimit: '0x0', gasUsed: '0x0', - size: '0x0', - number: '0x0' + size: '0x0' } } @@ -859,31 +929,35 @@ export class LoomProvider { }) } - private _onWebSocketMessage(msgEvent: IChainEventArgs) { - if (msgEvent.kind === ClientEvent.EVMEvent) { - log(`Socket message arrived ${JSON.stringify(msgEvent)}`) - this.notificationCallbacks.forEach((callback: Function) => { - 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), - address: msgEvent.contractAddress.local.toString(), - type: 'mined', - data: bytesToHexAddrLC(msgEvent.data), - topics: msgEvent.topics - } + private _onWebSocketMessage(msgEvent: IChainEventArgs | IJSONRPCEvent) { + if (this._client.isWeb3EndpointEnabled) { + log('Socket message arrived (web3)', JSON.stringify(msgEvent, null, 2)) + this.notificationCallbacks.forEach(callback => callback(msgEvent)) + return + } + + const eventArgs = msgEvent as IChainEventArgs + if (eventArgs.kind === ClientEvent.EVMEvent) { + const JSONRPCResult = { + jsonrpc: '2.0', + method: 'eth_subscription', + params: { + subscription: msgEvent.id, + result: { + transactionHash: bytesToHexAddrLC(eventArgs.transactionHashBytes), + logIndex: '0x0', + transactionIndex: '0x0', + blockHash: '0x0', + blockNumber: numberToHexLC(+eventArgs.blockHeight), + address: eventArgs.contractAddress.local.toString(), + type: 'mined', + data: bytesToHexAddrLC(eventArgs.data), + topics: eventArgs.topics } } - - callback(JSONRPCResult) - }) + } + log('Socket message arrived', JSON.stringify(JSONRPCResult, null, 2)) + this.notificationCallbacks.forEach(callback => callback(JSONRPCResult)) } } diff --git a/src/tests/e2e/client-evm-tests.ts b/src/tests/e2e/client-evm-tests.ts index f97da354..8ae98fa5 100644 --- a/src/tests/e2e/client-evm-tests.ts +++ b/src/tests/e2e/client-evm-tests.ts @@ -1,7 +1,7 @@ import test from 'tape' import { CryptoUtils } from '../../index' -import { createTestClient, execAndWaitForMillisecondsAsync } from '../helpers' +import { execAndWaitForMillisecondsAsync, createTestClient } from '../helpers' import { EthBlockHashList, EthBlockInfo } from '../../proto/evm_pb' import { bytesToHexAddr } from '../../crypto-utils' import { createDefaultTxMiddleware } from '../../helpers' @@ -23,7 +23,6 @@ test('Client EVM test (newBlockEvmFilterAsync)', async t => { if (!filterId) { t.fail('Filter Id cannot be null') } - // calls getevmfilterchanges const hash = await execAndWaitForMillisecondsAsync( client.getEvmFilterChangesAsync(filterId as string) @@ -61,7 +60,6 @@ test('Client EVM test (newPendingTransactionEvmFilterAsync)', async t => { let client try { const privateKey = CryptoUtils.generatePrivateKey() - const publicKey = CryptoUtils.publicKeyFromPrivateKey(privateKey) client = createTestClient() client.on('error', err => t.error(err)) diff --git a/src/tests/e2e/contract-tests.ts b/src/tests/e2e/contract-tests.ts index 0697066c..40b6608f 100644 --- a/src/tests/e2e/contract-tests.ts +++ b/src/tests/e2e/contract-tests.ts @@ -116,7 +116,7 @@ async function testContractEvents(t: test.Test, createClient: () => Client) { client.disconnect() } -test('Contract', async t => { +test('BluePrint Contract', async t => { try { t.comment('Calls via HTTP') await testContractCalls(t, createTestHttpClient) diff --git a/src/tests/e2e/loom-provider-web3-child-events.ts b/src/tests/e2e/loom-provider-web3-child-events.ts index 4bcc8eb6..ee303415 100644 --- a/src/tests/e2e/loom-provider-web3-child-events.ts +++ b/src/tests/e2e/loom-provider-web3-child-events.ts @@ -1,7 +1,7 @@ import test, { Test } from 'tape' import { LocalAddress, CryptoUtils } from '../../index' -import { createTestClient, waitForMillisecondsAsync } from '../helpers' +import { createTestClient, waitForMillisecondsAsync, createWeb3TestClient } from '../helpers' import { LoomProvider } from '../../loom-provider' import { deployContract } from '../evm-helpers' @@ -90,11 +90,11 @@ async function testContracts(t: Test, contractB: any, contractA: any) { const value = 5 contractA.events.ContractAEvent({}, (_err: Error, event: any) => { - t.equal(event.returnValues.v, '5', 'Value returned should be 5') + t.equal(event.returnValues.v, '5', 'Value returned should be 5 (A)') }) contractB.events.ContractBEvent({}, (_err: Error, event: any) => { - t.equal(event.returnValues.v, '5', 'Value returned should be 5') + t.equal(event.returnValues.v, '5', 'Value returned should be 5 (B)') }) let tx = await contractA.methods.doEmit(value, contractB.options.address).send() @@ -152,9 +152,9 @@ async function testGanache(t: Test) { web3.currentProvider.connection.close() } -async function testLoomProvider(t: Test) { +async function testLoomProvider(t: Test, useEthEndpoint: boolean) { const privKey = CryptoUtils.generatePrivateKey() - const client = createTestClient() + const client = useEthEndpoint ? createWeb3TestClient() : createTestClient() const from = LocalAddress.fromPublicKey(CryptoUtils.publicKeyFromPrivateKey(privKey)).toString() const loomProvider = new LoomProvider(client, privKey) const web3 = new Web3(loomProvider) @@ -173,9 +173,14 @@ async function testLoomProvider(t: Test) { client.disconnect() } -test('LoomProvider + Web3 + Child contracts events', async t => { +async function testWeb3ChildContractEvents(t: any, useEthEndpoint: boolean) { t.plan(6) await testGanache(t) - await testLoomProvider(t) + await testLoomProvider(t, useEthEndpoint) t.end() -}) +} + +test('LoomProvider + Web3 + Child contracts events (/query)', (t: any) => + testWeb3ChildContractEvents(t, false)) +test('LoomProvider + Web3 + Child contracts events (/eth)', (t: any) => + testWeb3ChildContractEvents(t, true)) diff --git a/src/tests/e2e/loom-provider-web3-middlewares-tests.ts b/src/tests/e2e/loom-provider-web3-middlewares-tests.ts index 508f649c..61a43bf8 100644 --- a/src/tests/e2e/loom-provider-web3-middlewares-tests.ts +++ b/src/tests/e2e/loom-provider-web3-middlewares-tests.ts @@ -1,7 +1,7 @@ import test from 'tape' import { LocalAddress, CryptoUtils, Client } from '../../index' -import { createTestClient, waitForMillisecondsAsync } from '../helpers' +import { createTestClient, waitForMillisecondsAsync, createWeb3TestClient } from '../helpers' import { LoomProvider } from '../../loom-provider' import { deployContract } from '../evm-helpers' @@ -54,11 +54,11 @@ class SuperSimpleMiddlware implements ITxMiddlewareHandler { } } -test('LoomProvider + Web3 + Middleware', async t => { +async function testWeb3Middleware(t: any, useEthEndpoint: boolean) { t.plan(2) const privKey = CryptoUtils.generatePrivateKey() - const client = createTestClient() + const client = useEthEndpoint ? createWeb3TestClient() : createTestClient() const from = LocalAddress.fromPublicKey(CryptoUtils.publicKeyFromPrivateKey(privKey)).toString() // Using a super simple custom middleware @@ -139,4 +139,7 @@ test('LoomProvider + Web3 + Middleware', async t => { if (client) { client.disconnect() } -}) +} + +test('LoomProvider + Web3 + Middleware (/query)', (t: any) => testWeb3Middleware(t, false)) +test('LoomProvider + Web3 + Middleware (/eth)', (t: any) => testWeb3Middleware(t, true)) diff --git a/src/tests/e2e/loom-provider-web3-tests.ts b/src/tests/e2e/loom-provider-web3-tests.ts index 7cfa4297..ed6cad87 100644 --- a/src/tests/e2e/loom-provider-web3-tests.ts +++ b/src/tests/e2e/loom-provider-web3-tests.ts @@ -2,7 +2,7 @@ import test from 'tape' import BN from 'bn.js' import { LocalAddress, CryptoUtils } from '../../index' -import { createTestClient, waitForMillisecondsAsync } from '../helpers' +import { createTestClient, waitForMillisecondsAsync, createWeb3TestClient } from '../helpers' import { LoomProvider } from '../../loom-provider' import { deployContract } from '../evm-helpers' @@ -40,9 +40,9 @@ const Web3 = require('web3') * */ -const newContractAndClient = async () => { +const newContractAndClient = async (useEthEndpoint: boolean) => { const privKey = CryptoUtils.generatePrivateKey() - const client = createTestClient() + const client = useEthEndpoint ? createWeb3TestClient() : createTestClient() const from = LocalAddress.fromPublicKey(CryptoUtils.publicKeyFromPrivateKey(privKey)).toString() const loomProvider = new LoomProvider(client, privKey) const web3 = new Web3(loomProvider) @@ -87,9 +87,9 @@ const newContractAndClient = async () => { return { contract, client, web3, from, privKey } } -test('LoomProvider + Web3 + Event with not matching topic', async t => { +async function testWeb3MismatchedTopic(t: any, useEthEndpoint: boolean) { t.plan(2) - const { contract, client } = await newContractAndClient() + const { contract, client } = await newContractAndClient(useEthEndpoint) try { const newValue = 1 @@ -116,11 +116,11 @@ test('LoomProvider + Web3 + Event with not matching topic', async t => { if (client) { client.disconnect() } -}) +} -test('LoomProvider + Web3 + Multiple event topics', async t => { +async function testWeb3MultipleTopics(t: any, useEthEndpoint: boolean) { t.plan(3) - const { contract, client } = await newContractAndClient() + const { contract, client } = await newContractAndClient(useEthEndpoint) try { const newValue = 1 @@ -130,14 +130,13 @@ test('LoomProvider + Web3 + Multiple event topics', async t => { t.equal(+event.returnValues._value, newValue, `Return value should be ${newValue}`) } }) + await waitForMillisecondsAsync(1000) const tx = await contract.methods.set(newValue).send() t.equal(tx.status, '0x1', 'SimpleStore.set should return correct status') const resultOfGet = await contract.methods.get().call() t.equal(+resultOfGet, newValue, `SimpleStore.get should return correct value`) - - await waitForMillisecondsAsync(1000) } catch (err) { console.log(err) } @@ -147,10 +146,10 @@ test('LoomProvider + Web3 + Multiple event topics', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + Eth Sign', async t => { - const { client, web3, from, privKey } = await newContractAndClient() +async function testWeb3Sign(t: any, useEthEndpoint: boolean) { + const { client, web3, from, privKey } = await newContractAndClient(useEthEndpoint) try { const msg = '0xff' const result = await web3.eth.sign(msg, from) @@ -178,10 +177,10 @@ test('LoomProvider + Web3 + Eth Sign', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + Get version', async t => { - const { client, web3 } = await newContractAndClient() +async function testWeb3NetId(t: any, useEthEndpoint: boolean) { + const { client, web3 } = await newContractAndClient(useEthEndpoint) try { const chainIdHash = soliditySha3(client.chainId) .slice(2) @@ -199,10 +198,10 @@ test('LoomProvider + Web3 + Get version', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + getBlockNumber', async t => { - const { client, web3 } = await newContractAndClient() +async function testWeb3BlockNumber(t: any, useEthEndpoint: boolean) { + const { client, web3 } = await newContractAndClient(useEthEndpoint) try { const blockNumber = await web3.eth.getBlockNumber() t.assert(typeof blockNumber === 'number', 'Block number should be a number') @@ -215,14 +214,14 @@ test('LoomProvider + Web3 + getBlockNumber', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + getBlockByNumber', async t => { - const { client, web3 } = await newContractAndClient() +async function testWeb3BlockByNumber(t: any, useEthEndpoint: boolean) { + const { client, web3 } = await newContractAndClient(useEthEndpoint) try { const blockNumber = await web3.eth.getBlockNumber() const blockInfo = await web3.eth.getBlock(blockNumber, false) - t.equal(parseInt(blockInfo.blockNumber, 16), blockNumber, 'Block number should be equal') + t.equal(blockInfo.number, blockNumber, 'Block number should be equal') } catch (err) { console.log(err) } @@ -232,17 +231,17 @@ test('LoomProvider + Web3 + getBlockByNumber', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + getBlockHash', async t => { - const { client, web3 } = await newContractAndClient() +async function testWeb3BlockByHash(t: any, useEthEndpoint: boolean) { + const { client, web3 } = await newContractAndClient(useEthEndpoint) try { const blockNumber = await web3.eth.getBlockNumber() const blockInfo = await web3.eth.getBlock(blockNumber, false) - const blockInfoByHash = await web3.eth.getBlock(blockInfo.transactionHash, false) + const blockInfoByHash = await web3.eth.getBlock(blockInfo.hash, false) t.assert(blockInfoByHash, 'Should return block info by hash') - } catch (err) { - console.log(err) + } catch (error) { + console.error(error) } if (client) { @@ -250,10 +249,10 @@ test('LoomProvider + Web3 + getBlockHash', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + getGasPrice', async t => { - const { client, web3 } = await newContractAndClient() +async function testWeb3GasPrice(t: any, useEthEndpoint: boolean) { + const { client, web3 } = await newContractAndClient(useEthEndpoint) try { const gasPrice = await web3.eth.getGasPrice() t.equal(gasPrice, null, "Gas price isn't used on Loomchain") @@ -266,10 +265,10 @@ test('LoomProvider + Web3 + getGasPrice', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + getBalance', async t => { - const { client, web3, from } = await newContractAndClient() +async function testWeb3Balance(t: any, useEthEndpoint: boolean) { + const { client, web3, from } = await newContractAndClient(useEthEndpoint) try { const balance = await web3.eth.getBalance(from) t.equal(balance, '0', 'Default balance is 0') @@ -282,16 +281,20 @@ test('LoomProvider + Web3 + getBalance', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + getTransactionReceipt', async t => { - const { contract, client } = await newContractAndClient() +async function testWeb3TransactionReceipt(t: any, useEthEndpoint: boolean) { + const { contract, client } = await newContractAndClient(useEthEndpoint) try { const newValue = 1 const tx = await contract.methods.set(newValue).send() console.log('tx', tx) - t.assert(tx.events.NewValueSet.blockTime > 0, 'blockTime should be greater than 0') + // TODO: there is no blockTime property in tx.events.NewValueSet, it's a Loom extension that's + // not implemented on the /eth endpoint yet, re-enable this when we implement it again + if (!useEthEndpoint) { + t.assert(tx.events.NewValueSet.blockTime > 0, 'blockTime should be greater than 0') + } t.assert(tx.events.NewValueSet.blockHash > 0, 'blockHash should be greater than 0') t.equal(tx.status, '0x1', 'SimpleStore.set should return correct status') @@ -305,10 +308,10 @@ test('LoomProvider + Web3 + getTransactionReceipt', async t => { } t.end() -}) +} -test('LoomProvider + Web3 + Logs', async t => { - const { contract, client, web3 } = await newContractAndClient() +async function testWeb3PastEvents(t: any, useEthEndpoint: boolean) { + const { contract, client, web3 } = await newContractAndClient(useEthEndpoint) try { const newValue = 1 @@ -321,8 +324,11 @@ test('LoomProvider + Web3 + Logs', async t => { }) console.log('events', events) t.assert(events.length > 0, 'Should have more than 0 events') - t.assert(events[0].blockTime > 0, 'blockTime should be greater than 0') - + // TODO: there is no blockTime property on Ethereum events, it's a Loom extension that's + // not implemented on the /eth endpoint yet, re-enable this when we implement it again + if (!useEthEndpoint) { + t.assert(events[0].blockTime > 0, 'blockTime should be greater than 0') + } await waitForMillisecondsAsync(1000) } catch (err) { console.log(err) @@ -333,4 +339,34 @@ test('LoomProvider + Web3 + Logs', async t => { } t.end() -}) +} + +test('LoomProvider + Web3 + Event with not matching topic (/query)', (t: any) => + testWeb3MismatchedTopic(t, false)) +test('LoomProvider + Web3 + Event with not matching topic (/eth)', (t: any) => + testWeb3MismatchedTopic(t, true)) +test('LoomProvider + Web3 + Multiple event topics (/query)', (t: any) => + testWeb3MultipleTopics(t, false)) +test('LoomProvider + Web3 + Multiple event topics (/eth)', (t: any) => + testWeb3MultipleTopics(t, true)) +test('LoomProvider + Web3 + Eth Sign (/query)', (t: any) => testWeb3Sign(t, false)) +test('LoomProvider + Web3 + Eth Sign (/eth)', (t: any) => testWeb3Sign(t, true)) +test('LoomProvider + Web3 + Get version (/query)', (t: any) => testWeb3NetId(t, false)) +test('LoomProvider + Web3 + Get version (/eth)', (t: any) => testWeb3NetId(t, true)) +test('LoomProvider + Web3 + getBlockNumber (/query)', (t: any) => testWeb3BlockNumber(t, false)) +test('LoomProvider + Web3 + getBlockNumber (/eth)', (t: any) => testWeb3BlockNumber(t, true)) +test('LoomProvider + Web3 + getBlockByNumber (/query)', (t: any) => + testWeb3BlockByNumber(t, false)) +test('LoomProvider + Web3 + getBlockByNumber (/eth)', (t: any) => testWeb3BlockByNumber(t, true)) +test('LoomProvider + Web3 + getBlock by hash (/query)', (t: any) => testWeb3BlockByHash(t, false)) +test('LoomProvider + Web3 + getBlock by hash (/eth)', (t: any) => testWeb3BlockByHash(t, true)) +test('LoomProvider + Web3 + getGasPrice (/query)', (t: any) => testWeb3GasPrice(t, false)) +test('LoomProvider + Web3 + getGasPrice (/eth)', (t: any) => testWeb3GasPrice(t, true)) +test('LoomProvider + Web3 + getBalance (/query)', (t: any) => testWeb3Balance(t, false)) +test('LoomProvider + Web3 + getBalance (/eth)', (t: any) => testWeb3Balance(t, true)) +test('LoomProvider + Web3 + getTransactionReceipt (/query)', (t: any) => + testWeb3TransactionReceipt(t, false)) +test('LoomProvider + Web3 + getTransactionReceipt (/eth)', (t: any) => + testWeb3TransactionReceipt(t, true)) +test('LoomProvider + Web3 + Logs (/query)', (t: any) => testWeb3PastEvents(t, false)) +test('LoomProvider + Web3 + Logs (/eth)', (t: any) => testWeb3PastEvents(t, true)) diff --git a/src/tests/e2e/multiple-events-nd-tests.ts b/src/tests/e2e/multiple-events-nd-tests.ts index 21c79f83..0072b58b 100644 --- a/src/tests/e2e/multiple-events-nd-tests.ts +++ b/src/tests/e2e/multiple-events-nd-tests.ts @@ -1,7 +1,7 @@ import test from 'tape' import { LocalAddress, CryptoUtils } from '../../index' -import { createTestClient, waitForMillisecondsAsync } from '../helpers' +import { createTestClient, waitForMillisecondsAsync, createWeb3TestClient } from '../helpers' import { LoomProvider } from '../../loom-provider' import { deployContract } from '../evm-helpers' @@ -32,9 +32,9 @@ const Web3 = require('web3') * */ -const newContractAndClient = async () => { +const newContractAndClient = async (useEthEndpoint: boolean) => { const privKey = CryptoUtils.generatePrivateKey() - const client = createTestClient() + const client = useEthEndpoint ? createWeb3TestClient() : createTestClient() const from = LocalAddress.fromPublicKey(CryptoUtils.publicKeyFromPrivateKey(privKey)).toString() const loomProvider = new LoomProvider(client, privKey) const web3 = new Web3(loomProvider) @@ -261,9 +261,9 @@ const newContractAndClient = async () => { } } -test('LoomProvider + Web3', async t => { +async function testMultipleContractEvents(t: any, useEthEndpoint: boolean) { t.plan(3) // EXPECTS 3 ASSERTIONS - const { contract, client } = await newContractAndClient() + const { contract, client } = await newContractAndClient(useEthEndpoint) try { contract.events.Transfer({}, (err: any, event: any) => { @@ -290,4 +290,7 @@ test('LoomProvider + Web3', async t => { } t.end() -}) +} + +test('LoomProvider + Web3 (/query)', (t: any) => testMultipleContractEvents(t, false)) +test('LoomProvider + Web3 (/eth)', (t: any) => testMultipleContractEvents(t, true)) diff --git a/src/tests/helpers.ts b/src/tests/helpers.ts index f3f2b2a7..70d437ca 100644 --- a/src/tests/helpers.ts +++ b/src/tests/helpers.ts @@ -9,6 +9,15 @@ export function getTestUrls() { } } +export function getWeb3TestUrls() { + return { + wsWriteUrl: process.env.TEST_LOOM_DAPP_WS_WRITE_URL || 'ws://127.0.0.1:46658/websocket', + wsReadUrl: process.env.TEST_LOOM_DAPP_WS_READ_URL || 'ws://127.0.0.1:46658/eth', + httpWriteUrl: process.env.TEST_LOOM_DAPP_HTTP_WRITE_URL || 'http://127.0.0.1:46658/rpc', + httpReadUrl: process.env.TEST_LOOM_DAPP_HTTP_READ_URL || 'http://127.0.0.1:46658/eth' + } +} + /** * Creates a client for tests, the default read/write URLs can be overridden by setting the env vars * TEST_LOOM_DAPP_WRITE_URL and TEST_LOOM_DAPP_READ_URL. These env vars can be set by modifying @@ -18,6 +27,13 @@ export function createTestClient(): Client { return new Client('default', getTestUrls().wsWriteUrl, getTestUrls().wsReadUrl) } +/** + * Creates a client for tests that use the /eth endpoint to query EVM contracts. + */ +export function createWeb3TestClient(): Client { + return new Client('default', getWeb3TestUrls().wsWriteUrl, getWeb3TestUrls().wsReadUrl) +} + export function createTestHttpClient(): Client { const writer = createJSONRPCClient({ protocols: [{ url: getTestUrls().httpWriteUrl }] }) const reader = createJSONRPCClient({ protocols: [{ url: getTestUrls().httpReadUrl }] }) diff --git a/src/tests/unit/rpc-client-factory-tests.ts b/src/tests/unit/rpc-client-factory-tests.ts index ba2d4b66..40a7bf58 100644 --- a/src/tests/unit/rpc-client-factory-tests.ts +++ b/src/tests/unit/rpc-client-factory-tests.ts @@ -4,7 +4,6 @@ import { createJSONRPCClient } from '../../index' import { HTTPRPCClient } from '../../internal/http-rpc-client' import { WSRPCClient } from '../../internal/ws-rpc-client' import { DualRPCClient } from '../../internal/dual-rpc-client' -import { RPCClientEvent } from '../../internal/json-rpc-client' test('RPC Client Factory', t => { try { diff --git a/src/types/rpcwebsockets.d.ts b/src/types/rpcwebsockets.d.ts index 1b395ee0..d7214137 100644 --- a/src/types/rpcwebsockets.d.ts +++ b/src/types/rpcwebsockets.d.ts @@ -15,7 +15,7 @@ declare module 'rpc-websockets' { constructor( address: string, options?: IClientOptions, - generate_request_id?: (method: string, params: object | any[]) => string + generate_request_id?: (method: string, params: object | any[]) => string | number ) call(method: string, params?: any, timeout?: number, options?: any): Promise on(event: string, listener: (...args: any[]) => void): this