2023-04-17 21:56:57 +00:00
|
|
|
// Types
|
|
|
|
import { Relayer as Types, RelayerOptions } from 'types/sdk/web'
|
|
|
|
import { RelayerProperties } from 'types/sdk/data'
|
2023-04-11 19:36:32 +00:00
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
// HTTP and proxy
|
|
|
|
import axios from 'axios'
|
|
|
|
import { AxiosInstance, AxiosError, AxiosResponse } from 'axios'
|
2023-04-08 19:17:52 +00:00
|
|
|
import { SocksProxyAgent } from 'socks-proxy-agent'
|
2023-04-17 21:56:57 +00:00
|
|
|
|
|
|
|
// Ethers
|
2023-04-11 19:36:32 +00:00
|
|
|
import { BigNumber } from 'ethers'
|
2023-04-17 21:56:57 +00:00
|
|
|
import { Web3Provider, Networkish } from '@ethersproject/providers'
|
|
|
|
|
|
|
|
// Local
|
|
|
|
import { ErrorUtils } from 'lib/utils'
|
|
|
|
import { Cache, Docs } from 'lib/data'
|
2023-04-08 19:17:52 +00:00
|
|
|
|
|
|
|
// 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
|
2023-04-17 21:56:57 +00:00
|
|
|
// modified by the Tornado Team or whoever else conributed, seems to properly error
|
|
|
|
// out when Tor is not running.
|
2023-04-08 19:17:52 +00:00
|
|
|
const HttpProvider = require('web3-providers-http')
|
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
export interface ObfuscationOptions {
|
2023-04-08 19:17:52 +00:00
|
|
|
port?: number
|
|
|
|
headers?: { name: string; value: string }[]
|
2023-04-17 21:56:57 +00:00
|
|
|
rv?: string
|
2023-04-08 19:17:52 +00:00
|
|
|
}
|
|
|
|
|
2023-04-11 19:36:32 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2023-04-08 19:17:52 +00:00
|
|
|
export class TorProvider extends Web3Provider {
|
2023-04-17 21:56:57 +00:00
|
|
|
constructor(url: string, torOpts?: ObfuscationOptions, network?: Networkish) {
|
|
|
|
torOpts = torOpts ?? {}
|
|
|
|
torOpts.rv = torOpts.rv ?? '102.0'
|
2023-04-08 19:17:52 +00:00
|
|
|
const torPort = torOpts.port ?? 9050,
|
|
|
|
headers = torOpts.headers ?? [
|
2023-04-17 21:56:57 +00:00
|
|
|
{
|
|
|
|
name: 'User-Agent',
|
|
|
|
value: `Mozilla/5.0 (Windows NT 10.0; rv:${torOpts.rv}) Gecko/20100101 Firefox/${torOpts.rv}`
|
|
|
|
}
|
2023-04-08 19:17:52 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
super(
|
|
|
|
new HttpProvider(url, {
|
2023-04-11 19:36:32 +00:00
|
|
|
// 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.
|
2023-04-19 17:01:40 +00:00
|
|
|
headers: typeof window !== 'undefined' ? headers : undefined,
|
|
|
|
// 1 minute timeout, although not sure if this is behaving properly
|
|
|
|
timeout: 60000
|
2023-04-08 19:17:52 +00:00
|
|
|
}),
|
|
|
|
network
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2023-04-11 19:36:32 +00:00
|
|
|
|
|
|
|
// @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
|
2023-04-17 21:56:57 +00:00
|
|
|
private _prices?: Map<string, BigNumber>
|
2023-04-11 19:36:32 +00:00
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
constructor(options: Types.Options, properties?: RelayerProperties) {
|
2023-04-11 19:36:32 +00:00
|
|
|
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
|
2023-04-17 21:56:57 +00:00
|
|
|
this._prices = properties.prices
|
2023-04-11 19:36:32 +00:00
|
|
|
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<RelayerProperties> {
|
|
|
|
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']
|
2023-04-19 17:01:40 +00:00
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
this._prices = Object.entries(properties['ethPrices']).reduce(
|
|
|
|
(map, entry) => map.set(entry[0], BigNumber.from(entry[1])),
|
|
|
|
new Map<string, BigNumber>()
|
|
|
|
)
|
2023-04-19 17:01:40 +00:00
|
|
|
|
2023-04-11 19:36:32 +00:00
|
|
|
this._fetched = true
|
|
|
|
|
|
|
|
return {
|
|
|
|
address: this._address!,
|
|
|
|
version: this._version!,
|
|
|
|
chainId: this._chainId!,
|
|
|
|
serviceFeePercent: this._serviceFee!,
|
|
|
|
miningFeePercent: this._miningFee!,
|
2023-04-17 21:56:57 +00:00
|
|
|
status: this._status!,
|
|
|
|
prices: this._prices!
|
2023-04-11 19:36:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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!
|
|
|
|
}
|
2023-04-17 21:56:57 +00:00
|
|
|
get prices(): Map<string, BigNumber> {
|
|
|
|
this._propertiesFetched('prices')
|
|
|
|
return this._prices!
|
|
|
|
}
|
2023-04-23 22:01:45 +00:00
|
|
|
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!
|
|
|
|
}
|
|
|
|
}
|
2023-04-11 19:36:32 +00:00
|
|
|
|
|
|
|
async getETHPurchasePrice(token: string): Promise<BigNumber> {
|
|
|
|
return BigNumber.from(
|
|
|
|
await this.httpClient
|
|
|
|
.get(this.url + '/status')
|
|
|
|
.catch((err) => {
|
|
|
|
throw ErrorUtils.ensureError(err)
|
|
|
|
})
|
|
|
|
.then((res) => res.data.prices[token])
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
async handleWithdrawal(
|
|
|
|
instanceAddress: string,
|
|
|
|
proof: Array<string>
|
|
|
|
): Promise<Types.WithdrawalRequestResult> {
|
|
|
|
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: Types.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
|
2023-04-11 19:36:32 +00:00
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
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
|
|
|
|
}
|
2023-04-11 19:36:32 +00:00
|
|
|
|
2023-04-17 21:56:57 +00:00
|
|
|
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
|
2023-04-11 19:36:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Cache
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct a new Relayer by reading relayer data from cache.
|
|
|
|
*/
|
|
|
|
static async fromCache(options: RelayerOptions): Promise<Relayer> {
|
|
|
|
const cache = new Cache.Base<Docs.Relayer>('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<void> {
|
|
|
|
if (!this._fetched) await this.fetchProperties()
|
|
|
|
|
|
|
|
const cache = new Cache.Base<Docs.Relayer>('Relayers')
|
|
|
|
|
|
|
|
const doc = new Docs.Relayer(this.url, {
|
|
|
|
address: this._address!,
|
|
|
|
version: this._version!,
|
|
|
|
chainId: this._chainId!,
|
|
|
|
serviceFeePercent: this._serviceFee!,
|
|
|
|
miningFeePercent: this._miningFee!,
|
2023-04-17 21:56:57 +00:00
|
|
|
status: this._status!,
|
|
|
|
prices: this._prices!
|
2023-04-11 19:36:32 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
await cache.db.put(doc).catch((err) => {
|
|
|
|
throw ErrorUtils.ensureError(err)
|
|
|
|
})
|
|
|
|
await cache.close().catch((err) => {
|
|
|
|
throw ErrorUtils.ensureError(err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|