// Types import { Options, WithdrawalRequestResult } from 'types/sdk/web' import { RelayerProperties } from 'types/sdk/data' // HTTP and proxy import axios from 'axios' import { AxiosInstance, AxiosError, AxiosResponse } from 'axios' import { SocksProxyAgent } from 'socks-proxy-agent' // Ethers import { BigNumber } from 'ethers' import { Web3Provider, Networkish } from '@ethersproject/providers' // Local import { ErrorUtils } from 'lib/utils' import { Cache, Docs } from 'lib/data' // It seems that the default HttpProvider offered by the normal web3 package // has some logic which either ignores the SocksProxyAgent or which falls back to // using no proxy for some reason. In any case, the Tornado-modified one, originally // modified by the Tornado Team or whoever else conributed, seems to properly error // out when Tor is not running. const HttpProvider = require('web3-providers-http') export interface ObfuscationOptions { port?: number headers?: { name: string; value: string }[] rv?: string } /** * You can also set up a SOCKS5 I2P tunnel on some port and then use that instead. Meaning that this should be compatible with I2P. */ export class TorProvider extends Web3Provider { constructor(url: string, torOpts?: ObfuscationOptions, network?: Networkish) { torOpts = torOpts ?? {} torOpts.rv = torOpts.rv ?? '102.0' const torPort = torOpts.port ?? 9050, headers = torOpts.headers ?? [ { name: 'User-Agent', value: `Mozilla/5.0 (Windows NT 10.0; rv:${torOpts.rv}) Gecko/20100101 Firefox/${torOpts.rv}` } ] super( new HttpProvider(url, { // The h after socks5 means that DNS resolution is also done through Tor agent: { https: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) }, // The XHR2 XMLHttpRequest assigns a Tor Browser header by itself. // But if in Browser we assign just in case. headers: typeof window !== 'undefined' ? headers : undefined, // 1 minute timeout, although not sure if this is behaving properly timeout: 60000 }), network ) } } // @ts-ignore export const TorHttpClient: new (opts?: { port?: number headers?: { [key: string]: string } rv?: string }) => AxiosInstance = function (opts?: { port?: number; headers?: { [key: string]: string }; rv?: string }) { const rv = opts?.rv ?? '102.0' return axios.create({ headers: opts?.headers ?? { 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; rv:${rv}) Gecko/20100101 Firefox/${rv}` }, httpsAgent: new SocksProxyAgent('socks5h://127.0.0.1:' + opts?.port ?? 9050), httpAgent: new SocksProxyAgent('socks5h://127.0.0.1:' + opts?.port ?? 9050), // 2 minute timeout timeout: 120000 }) } // @ts-ignore export const RegularHttpClient: new (opts?: any) => AxiosInstance = function (opts: any) { return axios.create(opts) } export class Relayer { url: string httpClient: AxiosInstance private _fetched: boolean private _address?: string private _version?: string private _serviceFee?: number private _miningFee?: number private _status?: string private _chainId?: number private _prices?: Map constructor(options: Options.Relayer, properties?: RelayerProperties) { this.url = options.url this.httpClient = options.httpClient this._fetched = false if (properties) { this._address = properties.address this._version = properties.version this._chainId = properties.chainId this._serviceFee = properties.serviceFeePercent this._miningFee = properties.miningFeePercent this._status = properties.status this._prices = properties.prices this._fetched = true } } // Setup /** * This function MUST be called to unlock the rest of the `Relayer` class functionality, as otherwise we don't have the property data necessary for all the logic we want. * @returns Fetched `RelayerProperties`. */ async fetchProperties(): Promise { const properties = await this.httpClient .get(this.url + '/status') .catch((err) => { throw ErrorUtils.ensureError(err) }) .then((res) => res.data) if (Object.entries(properties).length === 0) throw ErrorUtils.getError( 'Relayer.fetchProperties: Something went wrong with fetching properties from relayer endpoint.' ) this._address = properties['rewardAccount'] this._version = properties['version'] this._chainId = properties['netId'] this._serviceFee = properties['tornadoServiceFee'] this._miningFee = properties['miningFee'] this._status = properties['health']['status'] this._prices = Object.entries(properties['ethPrices']).reduce( (map, entry) => map.set(entry[0], BigNumber.from(entry[1])), new Map() ) this._fetched = true return { address: this._address!, version: this._version!, chainId: this._chainId!, serviceFeePercent: this._serviceFee!, miningFeePercent: this._miningFee!, status: this._status!, prices: this._prices! } } private _propertiesFetched(parentCallName: string): void { if (!this._fetched) throw ErrorUtils.getError( `Relayer.${parentCallName}: properties must be fetched first with \`fetchProperties\`.` ) } // Getters get address(): string { this._propertiesFetched('address') return this._address! } get version(): string { this._propertiesFetched('version') return this._version! } get serviceFeePercent(): number { this._propertiesFetched('serviceFee') return this._serviceFee! } get miningFeePercent(): number { this._propertiesFetched('miningFee') return this._miningFee! } get status(): string { this._propertiesFetched('status') return this._status! } get chainId(): number { this._propertiesFetched('chainId') return this._chainId! } get prices(): Map { this._propertiesFetched('prices') return this._prices! } get properties(): RelayerProperties { this._propertiesFetched('properties') return { address: this._address!, version: this._version!, chainId: this._chainId!, serviceFeePercent: this._serviceFee!, miningFeePercent: this._miningFee!, status: this._status!, prices: this._prices! } } async getETHPurchasePrice(token: string): Promise { return BigNumber.from( await this.httpClient .get(this.url + '/status') .catch((err) => { throw ErrorUtils.ensureError(err) }) .then((res) => res.data.prices[token]) ) } async handleWithdrawal(instanceAddress: string, proof: Array): Promise { const response = (await this.httpClient .post(this.url + '/v1/tornadoWithdraw', { contract: instanceAddress, proof: proof[0], args: proof.slice(1) }) .catch(this._handleHTTPError)) as AxiosResponse const { id } = response.data let result: WithdrawalRequestResult = { success: false }, finished = false while (!finished) { const statusResponse = (await this.httpClient .get(this.url + '/v1/jobs/' + id) .catch((err) => this._handleHTTPError(err, false))) as AxiosResponse if (statusResponse.status === 200) { const { txHash, status, _, failedReason } = statusResponse.data if (status === 'FAILED') { console.error(`\nRelayer.handleWithdrawal: withdrawal failed with reason: ${failedReason}\n`) finished = true } if (status == 'CONFIRMED') { result.success = true result.txHash = txHash finished = true } } await new Promise((resolve) => setTimeout(resolve, 3000)) } return result } private _handleHTTPError(err: AxiosError, httpThrow: boolean = true): void { err = ErrorUtils.ensureError(err) if (err.response) { if (httpThrow) // @ts-expect-error throw ErrorUtils.ensureError(err.response.data.error) // @ts-expect-error else console.error(err.response.data.error) } else if (err.request) { if (httpThrow) throw ErrorUtils.ensureError(err.request) else console.error(err.request) } else throw err } // Cache /** * Construct a new Relayer by reading relayer data from cache. */ static async fromCache(options: Options.Relayer): Promise { const cache = new Cache.Base('Relayers') // Error is ensured already const properties = await cache.get([options.url]).catch(() => { throw ErrorUtils.getError(`Relayer.fromCache: relayer ${options.url} isn't stored in cache.`) }) return new Relayer(options, properties) } /** * Cache relayer data into a PouchDB database in your cache folder. This will automatically fetch properties if they are not fetched. */ async remember(): Promise { if (!this._fetched) await this.fetchProperties() const cache = new Cache.Base('Relayers') const doc = new Docs.Relayer(this.url, { address: this._address!, version: this._version!, chainId: this._chainId!, serviceFeePercent: this._serviceFee!, miningFeePercent: this._miningFee!, status: this._status!, prices: this._prices! }) await cache.db.put(doc).catch((err) => { throw ErrorUtils.ensureError(err) }) await cache.close().catch((err) => { throw ErrorUtils.ensureError(err) }) } }