sdk-monorepo/src/lib/web.ts
2023-04-27 17:34:22 +00:00

315 lines
9.5 KiB
TypeScript

// 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<string, BigNumber>
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<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']
this._prices = Object.entries(properties['ethPrices']).reduce(
(map, entry) => map.set(entry[0], BigNumber.from(entry[1])),
new Map<string, BigNumber>()
)
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<string, BigNumber> {
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<BigNumber> {
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<string>): Promise<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: 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<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!,
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)
})
}
}