sdk-monorepo/src/lib/web.ts

231 lines
6.9 KiB
TypeScript
Raw Normal View History

import axios from 'axios'
import { AxiosInstance } from 'axios'
import { SocksProxyAgent } from 'socks-proxy-agent'
import { Web3Provider, Networkish } from '@ethersproject/providers'
import { RelayerOptions } from 'types/sdk/web'
import { BigNumber } from 'ethers'
import { ErrorUtils } from './utils'
import { Cache, Docs } from './data'
import { RelayerProperties } from 'types/sdk/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 who else, seems to properly error out when Tor
// is not running.
const HttpProvider = require('web3-providers-http')
export interface TorOptions {
port?: number
headers?: { name: string; value: 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: TorOptions, network?: Networkish) {
const torPort = torOpts.port ?? 9050,
headers = torOpts.headers ?? [
{ name: 'User-Agent', value: 'Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0' }
]
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
}),
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
constructor(options: RelayerOptions, 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._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._fetched = true
return {
address: this._address!,
version: this._version!,
chainId: this._chainId!,
serviceFeePercent: this._serviceFee!,
miningFeePercent: this._miningFee!,
status: this._status!
}
}
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!
}
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])
)
}
// TODO: Relaying stuff and related
async relay(): Promise<any> {}
async calcWithdrawalFee(token: string, denomination: number): Promise<BigNumber> {
//placeholder
return BigNumber.from(0)
}
// 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!,
status: this._status!
})
await cache.db.put(doc).catch((err) => {
throw ErrorUtils.ensureError(err)
})
await cache.close().catch((err) => {
throw ErrorUtils.ensureError(err)
})
}
}