diff --git a/README.md b/README.md index 71cbfaf..bd7aa8b 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ const provider = thor.ethers.modifyProvider( ) ) ``` -Obtaining a singner +Obtaining a signer ```ts const signer = provider.getSigner(address) ``` @@ -179,10 +179,12 @@ Supported subscription type: `newHeads`, `logs` Equivalent to `eth_chainId` ##### `web3_clientVersion` Returning string `thor` +##### `debug_traceTransaction` +##### `debug_traceCall` ## Implementation Notes 1. Fields `blockHash` and `transactionHash` return the values of [`blockId`](https://docs.vechain.org/thor/learn/block.html#block) and [`transactionId`](https://docs.vechain.org/thor/learn/transaction-model.html#transaction-id) defined in the Thor protocol, respectively -2. APIs `eth_estimateGas`, `eth_call` and `eth_getTransactionReceipt` only return information associated with the first [clause](https://docs.vechain.org/thor/learn/transaction-model.html#clauses) in a transaction +2. APIs `eth_estimateGas`, `eth_call`, `eth_getTransactionReceipt`, `debug_traceTransaction` and `debug_traceCall` only return information associated with the first [clause](https://docs.vechain.org/thor/learn/transaction-model.html#clauses) in a transaction 3. Unsupported returning fields (all set to zero): * `cumulativeGasUsed` * `difficulty` @@ -195,6 +197,6 @@ Returning string `thor` ## License This software is licensed under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.html), also included -in *LICENSE##### file in repository. +in *LICENSE* file in repository. ## References [1] [https://eth.wiki/json-rpc/API](https://eth.wiki/json-rpc/API). diff --git a/src/common.ts b/src/common.ts index ae5be7b..f2b5a8a 100644 --- a/src/common.ts +++ b/src/common.ts @@ -56,5 +56,8 @@ export const EthJsonRpcMethods = [ 'eth_subscribe', 'eth_unsubscribe', + 'debug_traceTransaction', + 'debug_traceCall', + 'evm_mine' ] \ No newline at end of file diff --git a/src/formatter.ts b/src/formatter.ts index 2d59990..764aae2 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -43,6 +43,7 @@ export class Formatter { this._inputFormatters['eth_getLogs'] = this._getLogs; this._inputFormatters['eth_subscribe'] = this._subscribe; this._inputFormatters['eth_sendRawTransaction'] = this._sendRawTransaction; + this._inputFormatters['debug_traceCall'] = this._traceCall; } formatInput = (method: string, params?: any[]): any[] => { @@ -155,6 +156,7 @@ export class Formatter { data: data, }], gas: o1.gas ? hexToNumber(o1.gas) : undefined, + gasPrice: o1.gasPrice, caller: o1.from, } @@ -233,6 +235,32 @@ export class Formatter { return [out]; } + private _traceCall = (params: any[]) => { + // trace call needs net, bypass if net not set + if (!this._ifSetNet) { + return params; + } + + let [callObj, revision = 'latest', opt] = params; + + revision = parseBlockNumber(revision); + if (revision === null) { + const msg = ErrMsg.ArgumentMissingOrInvalid('debug_traceCall', 'revision'); + throw new ProviderRpcError(ErrCode.InvalidParams, msg); + } + + const arg = { + to: callObj.to || null, + value: callObj.value || '0x0', + data: callObj.data || '0x', + gas: callObj.gas ? hexToNumber(callObj.gas) : undefined, + gasPrice: callObj.gasPrice, + caller: callObj.from, + }; + + return [arg, revision, opt]; + } + private _subscribe = (params: any[]) => { const name: string = params[0]; switch (name) { diff --git a/src/provider.ts b/src/provider.ts index aa5568f..dc16c6a 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -70,6 +70,8 @@ export class Provider extends EventEmitter implements IProvider { if (opt.net) { this.restful = new Restful(opt.net, this.connex.thor.genesis.id); this._methodMap['eth_sendRawTransaction'] = this._sendRawTransaction; + this._methodMap['debug_traceTransaction'] = this._traceTransaction; + this._methodMap['debug_traceCall'] = this._traceCall; } if (opt.wallet) { @@ -591,4 +593,77 @@ export class Provider extends EventEmitter implements IProvider { return Promise.reject(new ProviderRpcError(ErrCode.InternalError, getErrMsg(err))); } } + + private _traceTransaction = async (params: any[]) => { + /** + * debug_traceTransaction(txHash, traceOptions) + * traceOptions: { + * tracer: '', // name of tracer or custom js tracer code + * config: {} // struct logger config object + * tracerConfig: {} // tracer specific config object + * } + */ + try { + const txId = params[0] + const opts = params[1] + const tx = await this.connex.thor.transaction(txId).get(); + if (!tx) { return Promise.reject(new ProviderRpcError(ErrCode.Default, 'Target not found')); } + + const blk = (await this.connex.thor.block(tx.meta.blockID).get())!; + const txIndex = blk.transactions.findIndex(elem => elem == txId); + + + if (opts && opts.tracer) { + return this.restful!.traceClause({ + target: `${tx.meta.blockID}/${txIndex}/0`, + name: opts?.tracer, + config: opts?.tracerConfig, + }) + } else { + // if tracerConfig.name not specified, it's struct logger + // struct logger config is located at tracerConfig.config + return this.restful!.traceClause({ + target: `${tx.meta.blockID}/${txIndex}/0`, + name: opts?.tracer, + config: opts?.config, + }) + } + } catch (err: any) { + return Promise.reject(new ProviderRpcError(ErrCode.InternalError, getErrMsg(err))); + } + } + + private _traceCall = async (params: any[]) => { + /** + * debug_traceCall(callArgs, blockHashOrNumber ,tracerOptions) + * tracerOptions: { + * tracer: '', // name of tracer or custom js tracer code + * config: {} // struct logger config object + * tracerConfig: {} // tracer specific config object + * } + */ + try { + const callArgs = params[0] + const revision = params[1] + const opts = params[2] + + if (opts && opts.tracer) { + return this.restful!.traceCall({ + ... callArgs, + name: opts?.tracer, + config: opts?.tracerConfig, + }, revision) + } else { + // if tracerConfig.name not specified, it's struct logger + // struct logger config is located at tracerConfig.config + return this.restful!.traceCall({ + ... callArgs, + name: opts.tracer, + config: opts.config, + }, revision) + } + } catch (err: any) { + return Promise.reject(new ProviderRpcError(ErrCode.InternalError, getErrMsg(err))); + } + } } \ No newline at end of file diff --git a/src/restful.ts b/src/restful.ts index 50866b1..ac0e8ab 100644 --- a/src/restful.ts +++ b/src/restful.ts @@ -1,7 +1,7 @@ 'use strict'; import { ErrCode } from './error'; -import { Net, ExplainArg } from './types'; +import { Net, ExplainArg, TraceClauseOption, TraceCallOption } from './types'; import { decodeRevertReason, getErrMsg } from './utils'; import { ProviderRpcError } from './eip1193'; @@ -136,4 +136,47 @@ export class Restful { return Promise.reject(new ProviderRpcError(ErrCode.InternalError, getErrMsg(err))); } } + + traceClause = async (opts: TraceClauseOption) =>{ + try { + const httpParams: Net.Params = { + body: opts, + validateResponseHeader: this._headerValidator + } + + const ret: object = await this._net.http( + "POST", + 'debug/tracers', + httpParams + ); + + + return ret; + } catch (err: any) { + return Promise.reject(new ProviderRpcError(ErrCode.InternalError, getErrMsg(err))); + } + } + + traceCall = async (opts: TraceCallOption, revision?: string) => { + try { + const httpParams: Net.Params = { + body: opts, + validateResponseHeader: this._headerValidator + } + if (revision) { + httpParams.query = { "revision": revision }; + } + + const ret: object = await this._net.http( + "POST", + 'debug/tracers/call', + httpParams + ); + + + return ret; + } catch (err: any) { + return Promise.reject(new ProviderRpcError(ErrCode.InternalError, getErrMsg(err))); + } + } } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 6df6d13..87a61f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -181,7 +181,7 @@ export type TxObj = { value?: string; data?: string; gas?: string; - + gasPrice?: string; input?: string; // Added to support requests from web3.js } @@ -200,4 +200,22 @@ export type ConvertedFilterOpts = { export type DelegateOpt = { url: string; signer?: string; +} + +export interface TracerOption { + name: string; + config: object; +} + +export interface TraceClauseOption extends TracerOption { + target: string; +} + +export interface TraceCallOption extends TracerOption{ + to: string | null + value: string + data: string + caller?: string; + gas?: number; + gasPrice?: string; } \ No newline at end of file diff --git a/test/docker-compose-test.yaml b/test/docker-compose-test.yaml index 22d6462..1cf26b7 100644 --- a/test/docker-compose-test.yaml +++ b/test/docker-compose-test.yaml @@ -2,7 +2,7 @@ version: "3" services: thor-solo: - image: vechain/thor:v2.0.0 + image: vechain/thor:v2.1.0 container_name: thor-solo # Install curl for our healthcheck, then start thor solo entrypoint: @@ -11,6 +11,7 @@ services: "-c", "apk update && apk upgrade && apk add curl && thor solo --on-demand --data-dir /data/thor --api-addr 0.0.0.0:8669 --api-cors '*' --api-backtrace-limit -1 --verbosity 4" ] + user: root ports: - "8669:8669" - "11235:11235" diff --git a/test/provider/debug.test.ts b/test/provider/debug.test.ts new file mode 100644 index 0000000..03354d9 --- /dev/null +++ b/test/provider/debug.test.ts @@ -0,0 +1,263 @@ +import 'mocha' +import { expect, assert } from 'chai' +import { Driver, SimpleNet, SimpleWallet } from '@vechain/connex-driver' +import { soloAccounts, urls } from '../settings' +import { Provider } from '../../src' +import { Framework } from '@vechain/connex-framework' + +describe('Test debug namespace methods', function () { + let driver: Driver + let provider: Provider + + before(async () => { + const net = new SimpleNet(urls.testnet) + const wallet = new SimpleWallet() + + try { + driver = await Driver.connect(net, wallet) + const connex = new Framework(driver) + provider = new Provider({ + connex: connex, + net: net, + wallet: wallet + }) + } catch (err: any) { + assert.fail(err.message || err) + } + }) + + after(() => { + driver.close() + }) + + + it('no rest should throw error', async () => { + const p = new Provider({ connex: new Framework(driver) }) + + let error: Error | undefined = undefined + try { + await p.request({ method: 'debug_traceTransaction', params: [] }) + } catch (e) { + error = (e as Error) + } + expect(error).to.be.an('Error') + + error = undefined + try { + await p.request({ method: 'debug_traceCall', params: [] }) + } catch (e) { + error = (e as Error) + } + expect(error).to.be.an('Error') + + }) + + + it('invalid revision', async () => { + let error: Error | undefined = undefined + try { + await provider.request({ + method: 'debug_traceCall', params: [ + {}, 'x', {} + ] + }) + } catch (e) { + error = (e as Error) + } + expect(error).to.be.an('Error') + }) + + describe('Test trace transaction', function () { + it('call tracer', async () => { + let ret = await provider.request({ + method: 'debug_traceTransaction', + params: [ + '0x0050c856835d72c00974eb53fb249e261f31c4a4b1ca107e8fca8198f0fb7aa4', + { + tracer: 'call' + } + ] + }) + + expect(ret).to.haveOwnProperty('calls') + expect(ret.calls).to.be.an('array') + expect(ret).to.haveOwnProperty('type') + expect(ret.type).to.equal('CALL') + + ret = await provider.request({ + method: 'debug_traceTransaction', + params: [ + '0x0050c856835d72c00974eb53fb249e261f31c4a4b1ca107e8fca8198f0fb7aa4', + { + tracer: 'call', + tracerConfig: { + onlyTopCall: true + } + } + ] + }) + expect(ret).to.not.haveOwnProperty('calls') + }) + + it('struct logger', async () => { + let ret = await provider.request({ + method: 'debug_traceTransaction', + params: [ + '0x0050c856835d72c00974eb53fb249e261f31c4a4b1ca107e8fca8198f0fb7aa4', + { + tracer: '' + } + ] + }) + + expect(ret).to.haveOwnProperty('returnValue') + expect(ret).to.haveOwnProperty('structLogs') + expect(ret.structLogs).to.be.an('array') + expect(ret.structLogs[0]).to.haveOwnProperty('stack') + + ret = await provider.request({ + method: 'debug_traceTransaction', + params: [ + '0x0050c856835d72c00974eb53fb249e261f31c4a4b1ca107e8fca8198f0fb7aa4', + { + tracer: '', + config: { + disableStack: true + } + } + ] + }) + + expect(ret.structLogs[0]).to.not.haveOwnProperty('stack') + }) + }) + + describe('Test trace call', function () { + let solo: Provider + let soloDriver: Driver + + const wallet = new SimpleWallet() + wallet.import(soloAccounts[0]) + + before(async () => { + const net = new SimpleNet(urls.solo) + + try { + soloDriver = await Driver.connect(net, wallet) + const connex = new Framework(soloDriver) + solo = new Provider({ + connex: connex, + net: net, + wallet: wallet + }) + } catch (err: any) { + assert.fail(err.message || err) + } + }) + + after(function () { + soloDriver.close() + }) + + it('normalize args', async () => { + let ret = await solo.request({ + method: 'debug_traceCall', + params: [ + { + from: wallet.list[0].address, + gas: '0x100000', + }, + 'latest', + { + tracer: 'call', + } + ] + }) + expect(ret).to.not.haveOwnProperty('calls') + }) + + it('call tracer', async () => { + let ret = await solo.request({ + method: 'debug_traceCall', + params: [ + { + from: wallet.list[0].address, + to: '0x0000000000000000000000000000456e65726779', + data: '0xa9059cbb000000000000000000000000bec38ea2338a4dafc246eb7eaf1b81e8a15d635400000000000000000000000000000000000000000000003635c9adc5dea00000' + }, + 'latest', + { + tracer: 'call' + } + ] + }) + + expect(ret).to.haveOwnProperty('calls') + expect(ret.calls).to.be.an('array') + expect(ret).to.haveOwnProperty('type') + expect(ret.type).to.equal('CALL') + + ret = await solo.request({ + method: 'debug_traceCall', + params: [ + { + from: wallet.list[0].address, + to: '0x0000000000000000000000000000456e65726779', + data: '0xa9059cbb000000000000000000000000bec38ea2338a4dafc246eb7eaf1b81e8a15d635400000000000000000000000000000000000000000000003635c9adc5dea00000' + }, + 'latest', + { + tracer: 'call', + tracerConfig: { + onlyTopCall: true + } + } + ] + }) + expect(ret).to.not.haveOwnProperty('calls') + }) + + it('struct logger', async () => { + let ret = await solo.request({ + method: 'debug_traceCall', + params: [ + { + from: wallet.list[0].address, + to: '0x0000000000000000000000000000456e65726779', + data: '0xa9059cbb000000000000000000000000bec38ea2338a4dafc246eb7eaf1b81e8a15d635400000000000000000000000000000000000000000000003635c9adc5dea00000' + }, + 'latest', + { + tracer: '' + } + ] + }) + + expect(ret).to.haveOwnProperty('returnValue') + expect(ret).to.haveOwnProperty('structLogs') + expect(ret.structLogs).to.be.an('array') + expect(ret.structLogs[0]).to.haveOwnProperty('stack') + + ret = await solo.request({ + method: 'debug_traceCall', + params: [ + { + from: wallet.list[0].address, + to: '0x0000000000000000000000000000456e65726779', + data: '0xa9059cbb000000000000000000000000bec38ea2338a4dafc246eb7eaf1b81e8a15d635400000000000000000000000000000000000000000000003635c9adc5dea00000' + }, + 'latest', + { + tracer: '', + config: { + disableStack: true + } + } + ] + }) + + expect(ret.structLogs[0]).to.not.haveOwnProperty('stack') + }) + }) + +}) \ No newline at end of file diff --git a/test/settings.ts b/test/settings.ts index 4ae7e13..32c7620 100644 --- a/test/settings.ts +++ b/test/settings.ts @@ -15,8 +15,8 @@ for (let i = 0; i < 10; i++) { export { soloAccounts } export const urls = { - testnet: 'http://testnet.veblocks.net/', - mainnet: 'http://mainnet.veblocks.net/', + testnet: 'https://testnet.veblocks.net', + mainnet: 'https://mainnet.veblocks.net/', solo: 'http://127.0.0.1:8669/' }