import { TransactionInfo } from "types"; import fetch from "cross-fetch"; import { HttpProviderOptions, JsonRpcPayload, JsonRpcResponse } from 'web3-core-helpers'; import { Transform } from 'stream'; import http from 'http' import https from 'https' import zlib from 'zlib' const { parser } = require('stream-json') const { pick } = require('stream-json/filters/Pick') const { streamArray } = require('stream-json/streamers/StreamArray'); class BatchTransform extends Transform { buffer: any[] } export class SentioProvider { transactionInfo: TransactionInfo; chainId: string; host: string; txnHash: string; disableOptimizer: boolean; codeOverrides?: Record = {} constructor(host: string, options?: HttpProviderOptions) { this.host = host; } async init(txnHash: string, chainId: string, disableOptimizer: boolean, codeOverrides: Record) { this.chainId = chainId; this.txnHash = txnHash; this.disableOptimizer = disableOptimizer; this.codeOverrides = codeOverrides; const url = new URL("solidity/transaction_info", this.host); url.searchParams.append("txHash", txnHash); const resp = await fetch(url); this.transactionInfo = await resp.json(); } // TODO add tests getAffectedContract(txHash: string) { const url = new URL("solidity/affected_contract", this.host) url.searchParams.append("txHash", this.txnHash); return fetch(url).then(resp => resp.json().then(result => result.addresses)).catch(r => { throw new Error(r) }); } getDebugTraceStream(txHash: string, callback: (steps: any[]) => void) { const batchSize = 100 const batch = new Transform({ objectMode: true, transform(chunk, encoding, callback) { const that = this as BatchTransform; that.buffer = (that.buffer || []).concat(chunk); if (that.buffer.length >= batchSize) { this.push(that.buffer); that.buffer = []; } callback(); }, flush(callback) { const that = this as BatchTransform; if (that.buffer.length > 0) this.push(that.buffer); callback(); } }) let http2 try { http2 = require("http2") } catch (e) { } if (http2 && !this.host.includes("localhost")) { const session = http2.connect(this.host) session.on("error", (err: any) => { throw err }) const req = session.request({ ":path": `/api/v1/solidity/debug_trace_stream?txHash=${this.txnHash}&disableOptimizer=${this.disableOptimizer}`, "Accept-Encoding": "gzip, deflate" }) req.on("error", (err: any) => { throw err }) req.end() return new Promise((resolve, reject) => { const stream = req .pipe(zlib.createGunzip()) .pipe(parser() as Transform) .pipe(pick({filter: "result"}) as Transform) .pipe(pick({filter: "structLogs"}) as Transform) .pipe(streamArray() as Transform) .pipe(batch) stream.on("data", (steps: { key: number, value: any }[]) => { callback(steps.map(d => d.value)) }) stream.on("end", () => { session.close() resolve() }) stream.on("error", reject) }) } else { const [hostname, port] = this.host .replace(new RegExp("https*:\\/\\/"), "") .split("/")[0] .split(":") const options = { hostname, port, path: `/api/v1/solidity/debug_trace_stream?txHash=${this.txnHash}&disableOptimizer=${this.disableOptimizer}`, method: 'GET', } return new Promise((resolve, reject) => { const req = (this.host.startsWith("https") ? https : http).request(options, res => { if (res.statusCode != 200) { reject(`status code ${res.statusCode}`) return } const stream = res .pipe(parser() as Transform) .pipe(pick({filter: "result"}) as Transform) .pipe(pick({filter: "structLogs"}) as Transform) .pipe(streamArray() as Transform) .pipe(batch) stream.on("data", (steps: { key: number, value: any }[]) => { callback(steps.map(d => d.value)) }) stream.on("end", resolve) stream.on("error", reject) }) req.on('error', reject) req.end() }) } } send(payload: JsonRpcPayload, callback: (error: Error | null, result?: JsonRpcResponse | undefined) => void): void { let response: JsonRpcResponse = { id: payload.id!, jsonrpc: payload.jsonrpc, } switch (payload.method) { case "debug_traceTransaction": const url = new URL("solidity/debug_trace", this.host) url.searchParams.append("txHash", this.txnHash); fetch(url).then( resp => resp.json().then( result => { response.result = result; callback(null, response); })).catch(r => callback(new Error(r), undefined)); return; case "eth_getTransactionByHash": response.result = this.transactionInfo.transaction; break; case "eth_getTransactionReceipt": response.result = this.transactionInfo.transactionReceipt; break; case "eth_getBlockByNumber": response.result = this.transactionInfo.block; break; case "eth_chainId": response.result = this.chainId; break; case "eth_getCode": if (payload.params != null) { const contract = payload.params[0]; if (this.codeOverrides && this.codeOverrides[contract]) { response.result = "0x" + this.codeOverrides[contract] } else { const blockNumber = payload.params[1]; const matchedKey = Object.keys(this.transactionInfo.code).find(c => c.toLowerCase() === contract); if (matchedKey) { response.result = this.transactionInfo.code[matchedKey]; } else { try { const url = new URL("solidity/code", this.host); url.searchParams.append("contractAddress", contract); url.searchParams.append("blockNumber", blockNumber); fetch(url).then( resp => resp.json() .then( result => { response.result = result.code; callback(null, response); })).catch( r => callback(new Error(r), undefined)); } catch (e) { callback(e, undefined); } return; } } } else { callback(new Error("Get code request param is null or matched key is null.")); } break; case "debug_storageRangeAt": try { const url = new URL("solidity/storage_info", this.host); url.searchParams.append("blockHash", payload.params![0]); url.searchParams.append("txIdx", payload.params![1]); url.searchParams.append("contractAddress", payload.params![2]); url.searchParams.append("keyStart", payload.params![3]); url.searchParams.append("maxResult", payload.params![4]); fetch(url).then( resp => resp.json() .then( result => callback(null, result))).catch( r => callback(new Error(r), undefined)); } catch (e) { callback(e, undefined); } return; default: callback(new Error(`${payload.method} is not supported.`), undefined); return; } callback(null, response); } supportsSubscriptions(): boolean { return false; } disconnect() { } }