2023.04.20: Check HISTORY.md for more info
Signed-off-by: T-Hax <>
This commit is contained in:
parent
d83dcd8112
commit
4b661dd3e6
11
HISTORY.md
11
HISTORY.md
@ -1,5 +1,16 @@
|
|||||||
# History
|
# History
|
||||||
|
|
||||||
|
### 2023.04.20 (2023-04-20)
|
||||||
|
|
||||||
|
Did:
|
||||||
|
|
||||||
|
* Syncing is now stable.
|
||||||
|
* Reorganized some stuff including resources.
|
||||||
|
|
||||||
|
Next:
|
||||||
|
|
||||||
|
* Still testing withdraws, will do some relayer and then monorepo :] (finally excited about something again)
|
||||||
|
|
||||||
### 2023.04.18 (2023-04-18)
|
### 2023.04.18 (2023-04-18)
|
||||||
|
|
||||||
Did:
|
Did:
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"zk"
|
"zk"
|
||||||
],
|
],
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "2023.04.18",
|
"version": "2023.04.20",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18"
|
"node": "^18"
|
||||||
},
|
},
|
||||||
|
@ -22,7 +22,7 @@ import { BaseContract, BigNumber, ContractTransaction, providers, Signer, VoidSi
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
// Local modules
|
// Local modules
|
||||||
import { OnchainData } from 'lib/data'
|
import { Onchain } from 'lib/data'
|
||||||
import { ErrorUtils, HexUtils } from 'lib/utils'
|
import { ErrorUtils, HexUtils } from 'lib/utils'
|
||||||
|
|
||||||
// We use a vanilla provider here, but in reality we will probably
|
// We use a vanilla provider here, but in reality we will probably
|
||||||
@ -52,7 +52,7 @@ export class Chain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getChainSymbol(): Promise<string> {
|
async getChainSymbol(): Promise<string> {
|
||||||
if (!this.symbol) this.symbol = await OnchainData.getNetworkSymbol(String(await this.getChainId()))
|
if (!this.symbol) this.symbol = await Onchain.getNetworkSymbol(String(await this.getChainId()))
|
||||||
return this.symbol
|
return this.symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,12 +94,12 @@ export class Chain {
|
|||||||
): Promise<TransactionRequest> {
|
): Promise<TransactionRequest> {
|
||||||
if (callStruct[0].value)
|
if (callStruct[0].value)
|
||||||
return await Multicall3Contract__factory.connect(
|
return await Multicall3Contract__factory.connect(
|
||||||
await OnchainData.getMulticall3Address(String(this.chainId)),
|
await Onchain.getMulticall3Address(String(this.chainId)),
|
||||||
this.provider
|
this.provider
|
||||||
).populateTransaction.aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
).populateTransaction.aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
||||||
|
|
||||||
return await Multicall3Contract__factory.connect(
|
return await Multicall3Contract__factory.connect(
|
||||||
await OnchainData.getMulticall3Address(String(this.chainId)),
|
await Onchain.getMulticall3Address(String(this.chainId)),
|
||||||
this.provider
|
this.provider
|
||||||
).populateTransaction.aggregate3(callStruct)
|
).populateTransaction.aggregate3(callStruct)
|
||||||
}
|
}
|
||||||
@ -110,12 +110,12 @@ export class Chain {
|
|||||||
if (this.signer)
|
if (this.signer)
|
||||||
if (callStruct[0].value)
|
if (callStruct[0].value)
|
||||||
return await Multicall3Contract__factory.connect(
|
return await Multicall3Contract__factory.connect(
|
||||||
await OnchainData.getMulticall3Address(String(this.chainId)),
|
await Onchain.getMulticall3Address(String(this.chainId)),
|
||||||
this.signer
|
this.signer
|
||||||
).aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
).aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
||||||
else {
|
else {
|
||||||
return await Multicall3Contract__factory.connect(
|
return await Multicall3Contract__factory.connect(
|
||||||
await OnchainData.getMulticall3Address(String(this.chainId)),
|
await Onchain.getMulticall3Address(String(this.chainId)),
|
||||||
this.provider
|
this.provider
|
||||||
).aggregate3(callStruct)
|
).aggregate3(callStruct)
|
||||||
}
|
}
|
||||||
@ -156,11 +156,7 @@ export namespace Contracts {
|
|||||||
if (!contractMap.has(key)) {
|
if (!contractMap.has(key)) {
|
||||||
contractMap.set(
|
contractMap.set(
|
||||||
key,
|
key,
|
||||||
_getContract<TornadoProxy>(
|
_getContract<TornadoProxy>('TornadoProxy', await Onchain.getProxyAddress(network), signerOrProvider)
|
||||||
'TornadoProxy',
|
|
||||||
await OnchainData.getProxyAddress(network),
|
|
||||||
signerOrProvider
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return contractMap.get(`TornadoProxy${network}`) as TornadoProxy
|
return contractMap.get(`TornadoProxy${network}`) as TornadoProxy
|
||||||
@ -178,7 +174,7 @@ export namespace Contracts {
|
|||||||
key,
|
key,
|
||||||
_getContract<TornadoInstance>(
|
_getContract<TornadoInstance>(
|
||||||
'TornadoInstance',
|
'TornadoInstance',
|
||||||
await OnchainData.getInstanceAddress(network, token, denomination),
|
await Onchain.getInstanceAddress(network, token, denomination),
|
||||||
signerOrProvider
|
signerOrProvider
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
194
src/lib/core.ts
194
src/lib/core.ts
@ -1,5 +1,5 @@
|
|||||||
// ts-essentials
|
// ts-essentials
|
||||||
import { DeepRequired, MarkOptional, MarkRequired } from 'ts-essentials'
|
import { DeepRequired, MarkOptional } from 'ts-essentials'
|
||||||
|
|
||||||
// Local types
|
// Local types
|
||||||
import { RelayerProperties as RelayerDataProperties } from 'types/sdk/data'
|
import { RelayerProperties as RelayerDataProperties } from 'types/sdk/data'
|
||||||
@ -8,6 +8,7 @@ import { ZKDepositData, InputFor } from 'types/sdk/crypto'
|
|||||||
import { TornadoInstance, TornadoProxy } from 'types/deth'
|
import { TornadoInstance, TornadoProxy } from 'types/deth'
|
||||||
|
|
||||||
// External imports
|
// External imports
|
||||||
|
import { EventEmitter } from 'stream'
|
||||||
import { BigNumber, EventFilter, providers } from 'ethers'
|
import { BigNumber, EventFilter, providers } from 'ethers'
|
||||||
import { parseUnits } from 'ethers/lib/utils'
|
import { parseUnits } from 'ethers/lib/utils'
|
||||||
import { bigInt } from 'snarkjs'
|
import { bigInt } from 'snarkjs'
|
||||||
@ -16,10 +17,10 @@ import { bigInt } from 'snarkjs'
|
|||||||
import { parseIndexableString } from 'pouchdb-collate'
|
import { parseIndexableString } from 'pouchdb-collate'
|
||||||
|
|
||||||
// Local imports
|
// Local imports
|
||||||
import { Docs, Cache, Types as DataTypes, Json, Constants, OnchainData } from 'lib/data'
|
import { Docs, Cache, Types as DataTypes, Json, Constants, Onchain } from 'lib/data'
|
||||||
import { Primitives } from 'lib/crypto'
|
import { Primitives } from 'lib/crypto'
|
||||||
import { Contracts, Chain } from 'lib/chain'
|
import { Contracts, Chain } from 'lib/chain'
|
||||||
import { ErrorUtils, ObjectUtils } from 'lib/utils'
|
import { ErrorUtils, ObjectUtils, AsyncUtils } from 'lib/utils'
|
||||||
|
|
||||||
type Provider = providers.Provider
|
type Provider = providers.Provider
|
||||||
|
|
||||||
@ -36,12 +37,13 @@ type RelayerProperties = MarkOptional<
|
|||||||
'serviceFeePercent' | 'prices'
|
'serviceFeePercent' | 'prices'
|
||||||
>
|
>
|
||||||
|
|
||||||
export class Core {
|
export class Core extends EventEmitter {
|
||||||
chain: Chain
|
chain: Chain
|
||||||
caches: Map<string, Cache.Base<Docs.Base>>
|
caches: Map<string, Cache.Base<Docs.Base>>
|
||||||
instances: Map<string, TornadoInstance>
|
instances: Map<string, TornadoInstance>
|
||||||
|
|
||||||
constructor(provider: providers.Provider) {
|
constructor(provider: providers.Provider) {
|
||||||
|
super()
|
||||||
this.chain = new Chain(provider)
|
this.chain = new Chain(provider)
|
||||||
this.caches = new Map<string, Cache.Syncable<Docs.Base>>()
|
this.caches = new Map<string, Cache.Syncable<Docs.Base>>()
|
||||||
this.instances = new Map<string, TornadoInstance>()
|
this.instances = new Map<string, TornadoInstance>()
|
||||||
@ -110,8 +112,6 @@ export class Core {
|
|||||||
const hexNullifierHashes: string[] = []
|
const hexNullifierHashes: string[] = []
|
||||||
const purchaseAmounts = options?.ethPurchaseAmounts ?? new Array(zkDepositsData.length)
|
const purchaseAmounts = options?.ethPurchaseAmounts ?? new Array(zkDepositsData.length)
|
||||||
|
|
||||||
console.log('\nChecking inputs.\n')
|
|
||||||
|
|
||||||
if (zkDepositsData.length !== recipientAddresses.length)
|
if (zkDepositsData.length !== recipientAddresses.length)
|
||||||
throw ErrorUtils.getError(
|
throw ErrorUtils.getError(
|
||||||
'Core.buildDepositProofs: the number of recipients must equal the length of zkDepositsData.'
|
'Core.buildDepositProofs: the number of recipients must equal the length of zkDepositsData.'
|
||||||
@ -127,14 +127,10 @@ export class Core {
|
|||||||
hexNullifierHashes.push(deposit.hexNullifierHash)
|
hexNullifierHashes.push(deposit.hexNullifierHash)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nGetting lookup keys.\n')
|
|
||||||
|
|
||||||
// Determine cache name
|
// Determine cache name
|
||||||
const lookupKeys = await this.getInstanceLookupKeys(instance.address)
|
const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address)
|
||||||
const name = 'Deposits' + (lookupKeys.network + lookupKeys.token + lookupKeys.denomination).toUpperCase()
|
const name = 'Deposits' + (lookupKeys.network + lookupKeys.token + lookupKeys.denomination).toUpperCase()
|
||||||
|
|
||||||
console.log('\nLeaves and indices.\n')
|
|
||||||
|
|
||||||
// Find all leaves & indices by reading from cache
|
// Find all leaves & indices by reading from cache
|
||||||
const [leaves, leafIndices] = await this._findLeavesAndIndices(name, hexCommitments)
|
const [leaves, leafIndices] = await this._findLeavesAndIndices(name, hexCommitments)
|
||||||
const invalidCommitments: string[] = []
|
const invalidCommitments: string[] = []
|
||||||
@ -143,8 +139,6 @@ export class Core {
|
|||||||
const checkSpent = options?.checkNotesSpent !== false
|
const checkSpent = options?.checkNotesSpent !== false
|
||||||
const spentNotes: string[] = []
|
const spentNotes: string[] = []
|
||||||
|
|
||||||
console.log('\nNote checking.\n')
|
|
||||||
|
|
||||||
// If yes, immediately check it with the supplied Tornado Instance
|
// If yes, immediately check it with the supplied Tornado Instance
|
||||||
const checkSpentArray = checkSpent ? await instance.isSpentArray(hexNullifierHashes) : undefined
|
const checkSpentArray = checkSpent ? await instance.isSpentArray(hexNullifierHashes) : undefined
|
||||||
|
|
||||||
@ -161,8 +155,6 @@ export class Core {
|
|||||||
const commitmentsAreInvalid = invalidCommitments.length !== 0
|
const commitmentsAreInvalid = invalidCommitments.length !== 0
|
||||||
const notesAreSpent = spentNotes.length !== 0
|
const notesAreSpent = spentNotes.length !== 0
|
||||||
|
|
||||||
console.log('\nErrors.\n')
|
|
||||||
|
|
||||||
if (commitmentsAreInvalid || notesAreSpent)
|
if (commitmentsAreInvalid || notesAreSpent)
|
||||||
throw ErrorUtils.getError(
|
throw ErrorUtils.getError(
|
||||||
`Core.buildDepositProofs: ` +
|
`Core.buildDepositProofs: ` +
|
||||||
@ -176,8 +168,6 @@ export class Core {
|
|||||||
: '')
|
: '')
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('\nMerkle tree.\n')
|
|
||||||
|
|
||||||
// Otherwise, build the merkle tree from the leaves
|
// Otherwise, build the merkle tree from the leaves
|
||||||
const merkleTree = Primitives.buildMerkleTree({
|
const merkleTree = Primitives.buildMerkleTree({
|
||||||
height: options?.merkleTreeHeight ?? Constants.MERKLE_TREE_HEIGHT,
|
height: options?.merkleTreeHeight ?? Constants.MERKLE_TREE_HEIGHT,
|
||||||
@ -193,8 +183,6 @@ export class Core {
|
|||||||
'Core.buildDepositProofs: the merkle tree created is not valid, something went wrong with syncing.'
|
'Core.buildDepositProofs: the merkle tree created is not valid, something went wrong with syncing.'
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('\nProof data invariant.\n')
|
|
||||||
|
|
||||||
// Rest of note invariant arguments
|
// Rest of note invariant arguments
|
||||||
const inputsForProofs: InputFor.ZKProof[] = []
|
const inputsForProofs: InputFor.ZKProof[] = []
|
||||||
const gasPrice = options?.gasPrice ?? (await this.chain.getGasPrice())
|
const gasPrice = options?.gasPrice ?? (await this.chain.getGasPrice())
|
||||||
@ -207,7 +195,7 @@ export class Core {
|
|||||||
const decimals =
|
const decimals =
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
bigInt(10).pow(
|
bigInt(10).pow(
|
||||||
options?.tokenDecimals ?? (await OnchainData.getTokenDecimals(lookupKeys.network, lookupKeys.token))
|
options?.tokenDecimals ?? (await Onchain.getTokenDecimals(lookupKeys.network, lookupKeys.token))
|
||||||
)
|
)
|
||||||
const toWithdraw = BigNumber.from(lookupKeys.denomination).mul(decimals)
|
const toWithdraw = BigNumber.from(lookupKeys.denomination).mul(decimals)
|
||||||
|
|
||||||
@ -217,8 +205,6 @@ export class Core {
|
|||||||
'Core.buildDepositProofs: a token price MUST be supplied if the token withdrawn is not native.'
|
'Core.buildDepositProofs: a token price MUST be supplied if the token withdrawn is not native.'
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('\nConstruct.\n')
|
|
||||||
|
|
||||||
// Compute proofs
|
// Compute proofs
|
||||||
for (let i = 0, len = zkDepositsData.length; i < len; i++) {
|
for (let i = 0, len = zkDepositsData.length; i < len; i++) {
|
||||||
inputsForProofs.push({
|
inputsForProofs.push({
|
||||||
@ -248,8 +234,6 @@ export class Core {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nCalc and return.\n')
|
|
||||||
|
|
||||||
return await Primitives.calcDepositProofs(inputsForProofs)
|
return await Primitives.calcDepositProofs(inputsForProofs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,13 +269,13 @@ export class Core {
|
|||||||
if (indexes)
|
if (indexes)
|
||||||
for (let i = 0, len = rows.length; i < len; i++) {
|
for (let i = 0, len = rows.length; i < len; i++) {
|
||||||
const id = parseIndexableString(rows[i].id)[0]
|
const id = parseIndexableString(rows[i].id)[0]
|
||||||
if (id === indexes[i]) docs.push(rows[i].doc)
|
if (0 < indexes.findIndex(id)) docs.push(rows[i].doc)
|
||||||
}
|
}
|
||||||
else docs = rows.map((row) => row.doc)
|
else docs = rows.map((row) => row.doc)
|
||||||
|
|
||||||
if (keys)
|
if (keys)
|
||||||
docs.forEach((doc) => {
|
docs.forEach((doc) => {
|
||||||
const idNetworkMatches = doc && keys.network ? keys.network === doc?.network : true
|
const idNetworkMatches = doc && (keys.network ? keys.network === doc?.network : true)
|
||||||
const andTokenSymbolMatches = idNetworkMatches && (keys.token ? keys.token === doc?.token : true)
|
const andTokenSymbolMatches = idNetworkMatches && (keys.token ? keys.token === doc?.token : true)
|
||||||
const lastlyDenominationMatches =
|
const lastlyDenominationMatches =
|
||||||
andTokenSymbolMatches && (keys.denomination ? keys.denomination === doc?.denomination : true)
|
andTokenSymbolMatches && (keys.denomination ? keys.denomination === doc?.denomination : true)
|
||||||
@ -328,19 +312,19 @@ export class Core {
|
|||||||
options.backup.invoices = options.backup.invoices ?? true
|
options.backup.invoices = options.backup.invoices ?? true
|
||||||
options.backup.notes = options.backup.notes ?? true
|
options.backup.notes = options.backup.notes ?? true
|
||||||
options.doNotPopulate = options.doNotPopulate ?? true
|
options.doNotPopulate = options.doNotPopulate ?? true
|
||||||
return this.buildDepositTxs(instances, options)
|
return this.buildDepositTransactions(instances, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildDepositTx(
|
async buildDepositTransaction(
|
||||||
instance: TornadoInstance,
|
instance: TornadoInstance,
|
||||||
options?: Options.Core.Deposit
|
options?: Options.Core.Deposit
|
||||||
): Promise<Transactions.Deposit> {
|
): Promise<Transactions.Deposit> {
|
||||||
let opts: Options.Core.Deposit = options ?? {}
|
let opts: Options.Core.Deposit = options ?? {}
|
||||||
opts.depositsPerInstance = [1]
|
opts.depositsPerInstance = [1]
|
||||||
return (await this.buildDepositTxs([instance], opts))[0]
|
return (await this.buildDepositTransactions([instance], opts))[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildDepositTxs(
|
async buildDepositTransactions(
|
||||||
instances: Array<TornadoInstance>,
|
instances: Array<TornadoInstance>,
|
||||||
options?: Options.Core.Deposit
|
options?: Options.Core.Deposit
|
||||||
): Promise<Array<Transactions.Deposit>> {
|
): Promise<Array<Transactions.Deposit>> {
|
||||||
@ -363,7 +347,7 @@ export class Core {
|
|||||||
const proxy: TornadoProxy = await Contracts.getProxy(String(chainId), this.chain.provider)
|
const proxy: TornadoProxy = await Contracts.getProxy(String(chainId), this.chain.provider)
|
||||||
|
|
||||||
for (let i = 0, nInstances = instances.length; i < nInstances; i++) {
|
for (let i = 0, nInstances = instances.length; i < nInstances; i++) {
|
||||||
const lookupKeys = await this.getInstanceLookupKeys(instances[i].address)
|
const lookupKeys = await Onchain.getInstanceLookupKeys(instances[i].address)
|
||||||
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
|
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
|
||||||
|
|
||||||
for (let d = 0, nDeposits = depositsPerInstance[i]; d < nDeposits; d++) {
|
for (let d = 0, nDeposits = depositsPerInstance[i]; d < nDeposits; d++) {
|
||||||
@ -441,11 +425,25 @@ export class Core {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCache<T extends Docs.Base, C extends Cache.Base<T>>(cacheName: string): C {
|
loadWithdrawalCache(name: string, options?: Options.Core.Cache): Cache.Withdrawal {
|
||||||
if (!this.caches.has(cacheName)) {
|
if (!this.caches.has(name)) {
|
||||||
this.caches.set(cacheName, new Cache.Base<T>(cacheName))
|
this.caches.set(name, new Cache.Withdrawal(name, options))
|
||||||
}
|
}
|
||||||
return this.caches.get(cacheName) as C
|
return this.caches.get(name) as Cache.Withdrawal
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDepositCache(name: string, options?: Options.Core.Cache): Cache.Deposit {
|
||||||
|
if (!this.caches.has(name)) {
|
||||||
|
this.caches.set(name, new Cache.Deposit(name, options))
|
||||||
|
}
|
||||||
|
return this.caches.get(name) as Cache.Deposit
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCache<T extends Docs.Base, C extends Cache.Base<T>>(name: string, options?: Options.Cache.Database): C {
|
||||||
|
if (!this.caches.has(name)) {
|
||||||
|
this.caches.set(name, new Cache.Base<T>(name, options))
|
||||||
|
}
|
||||||
|
return this.caches.get(name) as C
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncMultiple(instances: Array<TornadoInstance>, syncOptions?: Options.Core.Sync): Promise<void> {
|
async syncMultiple(instances: Array<TornadoInstance>, syncOptions?: Options.Core.Sync): Promise<void> {
|
||||||
@ -456,7 +454,7 @@ export class Core {
|
|||||||
|
|
||||||
async sync(instance: TornadoInstance, syncOptions?: Options.Core.Sync): Promise<void> {
|
async sync(instance: TornadoInstance, syncOptions?: Options.Core.Sync): Promise<void> {
|
||||||
// Get some data
|
// Get some data
|
||||||
const lookupKeys = await this.getInstanceLookupKeys(instance.address)
|
const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address)
|
||||||
|
|
||||||
const populatedSyncOpts = await this._populateSyncOpts(lookupKeys, syncOptions)
|
const populatedSyncOpts = await this._populateSyncOpts(lookupKeys, syncOptions)
|
||||||
|
|
||||||
@ -464,46 +462,41 @@ export class Core {
|
|||||||
|
|
||||||
// Synchronize
|
// Synchronize
|
||||||
for (let i = 0, bound = actions.length; i < bound; i++) {
|
for (let i = 0, bound = actions.length; i < bound; i++) {
|
||||||
let action = actions[i][0].charAt(0).toUpperCase() + actions[i][0].slice(1)
|
const action = actions[i][0].charAt(0).toUpperCase() + actions[i][0].slice(1)
|
||||||
await this._sync(action, lookupKeys, instance, populatedSyncOpts)
|
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
|
||||||
|
const name = action + 's' + pathstring.toUpperCase()
|
||||||
|
|
||||||
|
if (action == 'Deposit')
|
||||||
|
await this._sync(
|
||||||
|
pathstring,
|
||||||
|
this.loadDepositCache(name, syncOptions?.cache),
|
||||||
|
instance.filters.Deposit(null, null, null),
|
||||||
|
instance,
|
||||||
|
populatedSyncOpts
|
||||||
|
)
|
||||||
|
else if (action == 'Withdrawal')
|
||||||
|
await this._sync(
|
||||||
|
pathstring,
|
||||||
|
this.loadWithdrawalCache(name, syncOptions?.cache),
|
||||||
|
instance.filters.Withdrawal(null, null, null, null),
|
||||||
|
instance,
|
||||||
|
populatedSyncOpts
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _sync(
|
private async _sync(
|
||||||
action: string,
|
pathstring: string,
|
||||||
lookupKeys: DataTypes.Keys.InstanceLookup,
|
cache: Cache.Syncable<Docs.Base>,
|
||||||
|
filter: EventFilter,
|
||||||
instance: TornadoInstance,
|
instance: TornadoInstance,
|
||||||
syncOptions: DeepRequired<Options.Core.Sync>
|
syncOptions: DeepRequired<Options.Core.Sync>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const name = `${action + 's'}${lookupKeys.network}${lookupKeys.token.toUpperCase()}${
|
|
||||||
lookupKeys.denomination
|
|
||||||
}`,
|
|
||||||
pathstring = name.substring(action.length).toLowerCase()
|
|
||||||
|
|
||||||
let cache: Cache.Syncable<Docs.Base>,
|
|
||||||
toDoc: (_: any) => Docs.Base,
|
|
||||||
filter: EventFilter,
|
|
||||||
numEntries: number
|
|
||||||
|
|
||||||
if (action == 'Deposit') {
|
|
||||||
toDoc = (resp: any) => new Docs.Deposit(resp)
|
|
||||||
cache = this.caches.has(name)
|
|
||||||
? (this.caches.get(name)! as Cache.Deposit)
|
|
||||||
: new Cache.Deposit(name, syncOptions.cache)
|
|
||||||
filter = instance.filters.Deposit(null, null, null)
|
|
||||||
} else {
|
|
||||||
toDoc = (resp: any) => new Docs.Withdrawal(resp)
|
|
||||||
cache = this.caches.has(name)
|
|
||||||
? (this.caches.get(name)! as Cache.Withdrawal)
|
|
||||||
: new Cache.Withdrawal(name, syncOptions.cache)
|
|
||||||
filter = instance.filters.Withdrawal(null, null, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign pooler
|
// Assign pooler
|
||||||
cache.sync.pooler = await cache.sync.initializePooler(cache.getCallbacks(instance))
|
cache.sync.initializePooler(cache.getCallbacks(instance), cache.getErrorHandlers())
|
||||||
|
|
||||||
// Decide whether we have a latest block
|
// Decide whether we have a latest block
|
||||||
numEntries = (await cache.db.info()).doc_count
|
const numEntries = (await cache.db.info()).doc_count
|
||||||
|
|
||||||
// Check for synced blocks
|
// Check for synced blocks
|
||||||
if (0 < numEntries) {
|
if (0 < numEntries) {
|
||||||
@ -518,6 +511,15 @@ export class Core {
|
|||||||
// Start synchronizing
|
// Start synchronizing
|
||||||
let dbPromises = []
|
let dbPromises = []
|
||||||
|
|
||||||
|
this.emit(
|
||||||
|
'debug',
|
||||||
|
syncOptions.blocks.startBlock,
|
||||||
|
syncOptions.blocks.targetBlock,
|
||||||
|
syncOptions.blocks.blockDelta
|
||||||
|
)
|
||||||
|
|
||||||
|
this.emit('sync', 'syncing')
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let currentBlock = syncOptions.blocks.startBlock,
|
let currentBlock = syncOptions.blocks.startBlock,
|
||||||
blockDelta = syncOptions.blocks.blockDelta,
|
blockDelta = syncOptions.blocks.blockDelta,
|
||||||
@ -526,37 +528,46 @@ export class Core {
|
|||||||
currentBlock < targetBlock;
|
currentBlock < targetBlock;
|
||||||
currentBlock += blockDelta
|
currentBlock += blockDelta
|
||||||
) {
|
) {
|
||||||
if (cache.sync.pooler.pending < concurrencyLimit) {
|
if (cache.sync.pooler!.pending < concurrencyLimit) {
|
||||||
const sum = currentBlock + blockDelta
|
const sum = currentBlock + blockDelta
|
||||||
|
|
||||||
|
await AsyncUtils.timeout(syncOptions.msTimeout)
|
||||||
|
|
||||||
if (currentBlock + blockDelta < targetBlock) {
|
if (currentBlock + blockDelta < targetBlock) {
|
||||||
await cache.sync.pooler.pool(currentBlock, sum)
|
await cache.sync.pooler!.pool(currentBlock, sum)
|
||||||
} else {
|
} else {
|
||||||
await cache.sync.pooler.pool(currentBlock, sum - (sum % targetBlock))
|
await cache.sync.pooler!.pool(currentBlock, sum - (sum % targetBlock))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('debug', currentBlock++, sum)
|
||||||
} else {
|
} else {
|
||||||
let res: Array<any> = await cache.sync.pooler.race()
|
let res: Array<any> = await cache.sync.pooler!.race()
|
||||||
|
|
||||||
if (res.length != 0)
|
if (res.length != 0)
|
||||||
dbPromises.push(
|
dbPromises.push(
|
||||||
cache.db.bulkDocs(res.map((el) => toDoc(el))).catch((err) => {
|
cache.db.bulkDocs(res.map((el) => cache.buildDoc(el))).catch((err) => {
|
||||||
throw ErrorUtils.ensureError(err)
|
throw ErrorUtils.ensureError(err)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
currentBlock -= blockDelta
|
currentBlock -= blockDelta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('sync', 'synced')
|
||||||
|
|
||||||
// Immediately start listening if we're doing this
|
// Immediately start listening if we're doing this
|
||||||
if (syncOptions.cache.sync.listen)
|
if (syncOptions.cache.sync.listen)
|
||||||
instance = instance.on(filter, (...eventArgs) => {
|
instance = instance.on(filter, (...eventArgs) => {
|
||||||
cache.db.put(toDoc(eventArgs[eventArgs.length - 1]))
|
cache.db.put(cache.buildDoc(eventArgs[eventArgs.length - 1]))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Then wait for all pooler requests to resolve
|
// Then wait for all pooler requests to resolve
|
||||||
let results = await cache.sync.pooler.all()
|
let results = await cache.sync.pooler!.all()
|
||||||
|
|
||||||
// Then transform them, we know the shape in forward
|
// Then transform them, we know the shape in forward
|
||||||
results = results.reduce((res: any[], response: any[]) => {
|
results = results.reduce((res: any[], response: any[]) => {
|
||||||
if (response[0]) response.forEach((el: any) => res.push(toDoc(el)))
|
if (response[0]) response.forEach((el: any) => res.push(cache.buildDoc(el)))
|
||||||
return res
|
return res
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -570,7 +581,7 @@ export class Core {
|
|||||||
|
|
||||||
// Finally, store the objects
|
// Finally, store the objects
|
||||||
if (!this.instances.has(pathstring)) this.instances.set(pathstring, instance)
|
if (!this.instances.has(pathstring)) this.instances.set(pathstring, instance)
|
||||||
if (!this.caches.has(name)) this.caches.set(name, cache)
|
if (!this.caches.has(cache.name)) this.caches.set(cache.name, cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _populateSyncOpts(
|
private async _populateSyncOpts(
|
||||||
@ -594,16 +605,16 @@ export class Core {
|
|||||||
// blocks
|
// blocks
|
||||||
syncOptions.blocks.startBlock =
|
syncOptions.blocks.startBlock =
|
||||||
syncOptions.blocks.startBlock ??
|
syncOptions.blocks.startBlock ??
|
||||||
(await OnchainData.getInstanceDeployBlockNum(
|
(await Onchain.getInstanceDeployBlockNum(lookupKeys.network, lookupKeys.token, lookupKeys.denomination))
|
||||||
lookupKeys.network,
|
|
||||||
lookupKeys.token,
|
|
||||||
lookupKeys.denomination
|
|
||||||
))
|
|
||||||
|
|
||||||
syncOptions.blocks.targetBlock = syncOptions.blocks.targetBlock ?? (await this.chain.latestBlockNum())
|
syncOptions.blocks.targetBlock = syncOptions.blocks.targetBlock ?? (await this.chain.latestBlockNum())
|
||||||
|
|
||||||
|
syncOptions.blocks.deltaDivisor = syncOptions.blocks.deltaDivisor ?? 100
|
||||||
|
|
||||||
syncOptions.blocks.blockDelta = this._getBlockDelta(syncOptions)
|
syncOptions.blocks.blockDelta = this._getBlockDelta(syncOptions)
|
||||||
|
|
||||||
|
syncOptions.msTimeout = syncOptions.msTimeout ?? 200 // 5 requests per second
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
// db
|
// db
|
||||||
syncOptions.cache.db.persistent = syncOptions.cache.db.persistent ?? true
|
syncOptions.cache.db.persistent = syncOptions.cache.db.persistent ?? true
|
||||||
@ -618,8 +629,8 @@ export class Core {
|
|||||||
|
|
||||||
private _getBlockDelta(syncOptions?: Options.Core.Sync): number {
|
private _getBlockDelta(syncOptions?: Options.Core.Sync): number {
|
||||||
return Math.floor(
|
return Math.floor(
|
||||||
syncOptions?.blocks?.blockDelta ??
|
(syncOptions!.blocks!.targetBlock! - syncOptions!.blocks!.startBlock!) /
|
||||||
(syncOptions!.blocks!.targetBlock! - syncOptions!.blocks!.startBlock!) / 20
|
syncOptions!.blocks!.deltaDivisor!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,25 +681,6 @@ export class Core {
|
|||||||
// Concat matched and all leaf indices
|
// Concat matched and all leaf indices
|
||||||
return [leaves, indices]
|
return [leaves, indices]
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstanceLookupKeys(instanceAddress: string): Promise<DataTypes.Keys.InstanceLookup> {
|
|
||||||
// lookup some stuff first
|
|
||||||
const lookupObj: { [key: string]: string } = Json.getValue(await Json.load('onchain/quickLookup.json'), [
|
|
||||||
'instanceAddresses'
|
|
||||||
])
|
|
||||||
|
|
||||||
const pathstring: string = Object.entries(lookupObj).find((el) => el[1] === instanceAddress)![0]
|
|
||||||
|
|
||||||
const network = pathstring.match('[0-9]+')![0],
|
|
||||||
token = pathstring.substring(network.length).match('[a-z]+')![0],
|
|
||||||
denomination = pathstring.substring(network.length + token.length)
|
|
||||||
|
|
||||||
return {
|
|
||||||
network: network,
|
|
||||||
token: token,
|
|
||||||
denomination: denomination
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Transactions, Options }
|
export { Transactions, Options }
|
||||||
|
110
src/lib/data.ts
110
src/lib/data.ts
@ -26,12 +26,15 @@ PouchDB.plugin(PouchDBAdapterMemory)
|
|||||||
export namespace Files {
|
export namespace Files {
|
||||||
export type PathGetter = (relative: string) => string
|
export type PathGetter = (relative: string) => string
|
||||||
|
|
||||||
export const getModulesPath = (relative: string): string => __dirname + '/../../node_modules/' + relative
|
export const getModulesPath = (relative: string, prefix?: string): string =>
|
||||||
export const getResourcePath = (relative: string): string => __dirname + '/../resources/' + relative
|
(prefix ?? __dirname + '/../../node_modules/') + relative
|
||||||
export const getCachePath = (relative: string): string => __dirname + '/../../cache/' + relative
|
export const getResourcePath = (relative: string, prefix?: string): string =>
|
||||||
|
(prefix ?? __dirname + '/../resources/') + relative
|
||||||
|
export const getCachePath = (relative: string, prefix?: string): string =>
|
||||||
|
(prefix ?? __dirname + '/../../cache/') + relative
|
||||||
|
|
||||||
export const cacheDirExists = (): boolean => existsSync(getCachePath(''))
|
export const cacheDirExists = (prefix?: string): boolean => existsSync(getCachePath('', prefix))
|
||||||
export const makeCacheDir = (): void => mkdirSync(getCachePath(''))
|
export const makeCacheDir = (prefix?: string): void => mkdirSync(getCachePath('', prefix))
|
||||||
|
|
||||||
export const loadRaw = (relative: string): Promise<Buffer> => readFile(getResourcePath(relative))
|
export const loadRaw = (relative: string): Promise<Buffer> => readFile(getResourcePath(relative))
|
||||||
|
|
||||||
@ -123,7 +126,7 @@ export namespace Json {
|
|||||||
|
|
||||||
// TODO: Decide whether to also cache the data instead of just loading it for the function call
|
// TODO: Decide whether to also cache the data instead of just loading it for the function call
|
||||||
|
|
||||||
export namespace OnchainData {
|
export namespace Onchain {
|
||||||
export async function getClassicInstanceData(
|
export async function getClassicInstanceData(
|
||||||
network: string,
|
network: string,
|
||||||
token: string,
|
token: string,
|
||||||
@ -141,24 +144,43 @@ export namespace OnchainData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInstanceAttrSet<T>(
|
export async function getInstanceLookupKeys(instanceAddress: string): Promise<Types.Keys.InstanceLookup> {
|
||||||
key: string,
|
// lookup some stuff first
|
||||||
|
const lookupObj: { [key: string]: string } = await Json.load('onchain/instanceAddresses.json')
|
||||||
|
|
||||||
|
const pathstring: string = Object.entries(lookupObj).find((el) => el[1] === instanceAddress)![0]
|
||||||
|
|
||||||
|
const network = pathstring.match('[0-9]+')![0],
|
||||||
|
token = pathstring.substring(network.length).match('[a-z]+')![0],
|
||||||
|
denomination = pathstring.substring(network.length + token.length)
|
||||||
|
|
||||||
|
return {
|
||||||
|
network: network,
|
||||||
|
token: token,
|
||||||
|
denomination: denomination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPathstringBasedContent<T>(
|
||||||
|
filepath: string,
|
||||||
paths: Array<{
|
paths: Array<{
|
||||||
network?: string
|
network?: string
|
||||||
token?: string
|
token?: string
|
||||||
denomination?: string
|
denomination?: string
|
||||||
}>
|
}>
|
||||||
): Promise<Array<T>> {
|
): Promise<Array<T>> {
|
||||||
const obj = await Json.load('onchain/quickLookup.json')
|
const obj = await Json.load(filepath)
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
paths.map((path) =>
|
paths.map((path) =>
|
||||||
Json.getValue(obj, [key, `${path.network ?? '\0'}${path.token ?? '\0'}${path.denomination ?? '\0'}`])
|
Json.getValue(obj, [`${path.network ?? '\0'}${path.token ?? '\0'}${path.denomination ?? '\0'}`])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNetworkSymbol(networkId: string): Promise<string> {
|
export async function getNetworkSymbol(networkId: string): Promise<string> {
|
||||||
return (await getInstanceAttrSet<string>('networkSymbols', [{ network: networkId }]))[0]
|
return (
|
||||||
|
await getPathstringBasedContent<string>('onchain/networkSymbols.json', [{ network: networkId }])
|
||||||
|
)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInstanceAddresses(
|
export function getInstanceAddresses(
|
||||||
@ -168,7 +190,7 @@ export namespace OnchainData {
|
|||||||
denomination: string
|
denomination: string
|
||||||
}>
|
}>
|
||||||
): Promise<Array<string>> {
|
): Promise<Array<string>> {
|
||||||
return getInstanceAttrSet<string>('instanceAddresses', paths)
|
return getPathstringBasedContent<string>('onchain/instanceAddresses.json', paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInstanceAddress(
|
export async function getInstanceAddress(
|
||||||
@ -186,7 +208,7 @@ export namespace OnchainData {
|
|||||||
denomination: string
|
denomination: string
|
||||||
}>
|
}>
|
||||||
): Promise<Array<number>> {
|
): Promise<Array<number>> {
|
||||||
return getInstanceAttrSet<number>('deployedBlockNumber', paths)
|
return getPathstringBasedContent<number>('onchain/deployedBlockNumbers.json', paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInstanceDeployBlockNum(
|
export async function getInstanceDeployBlockNum(
|
||||||
@ -220,12 +242,22 @@ export namespace OnchainData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTokenAddress(network: string, token: string): Promise<string> {
|
||||||
|
return (
|
||||||
|
await getPathstringBasedContent<string>('onchain/tokenAddresses.json', [
|
||||||
|
{ network: network, token: token }
|
||||||
|
])
|
||||||
|
)[0]
|
||||||
|
}
|
||||||
|
|
||||||
export async function getTokenDecimals(network: string, token: string): Promise<number> {
|
export async function getTokenDecimals(network: string, token: string): Promise<number> {
|
||||||
return (await getTokenData(network, token)).decimals
|
return (
|
||||||
|
await getPathstringBasedContent<number>('onchain/decimals.json', [{ network: network, token: token }])
|
||||||
|
)[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace OffchainData {
|
export namespace Offchain {
|
||||||
export async function getUncensoredRpcURL(network: string, name: string = ''): Promise<string> {
|
export async function getUncensoredRpcURL(network: string, name: string = ''): Promise<string> {
|
||||||
const rpcs = Json.toMap<string>(
|
const rpcs = Json.toMap<string>(
|
||||||
Json.getValue(await Json.load('offchain/infrastructure.json'), ['jrpc-uncensored', network])
|
Json.getValue(await Json.load('offchain/infrastructure.json'), ['jrpc-uncensored', network])
|
||||||
@ -366,9 +398,12 @@ export namespace Docs {
|
|||||||
|
|
||||||
export namespace Cache {
|
export namespace Cache {
|
||||||
export class Base<T extends Docs.Base> {
|
export class Base<T extends Docs.Base> {
|
||||||
|
name: string
|
||||||
db: PouchDB.Database<T>
|
db: PouchDB.Database<T>
|
||||||
|
|
||||||
constructor(name: string, options?: Options.Cache.Database) {
|
constructor(name: string, options?: Options.Cache.Database) {
|
||||||
|
this.name = name
|
||||||
|
|
||||||
if (options?.persistent === false && options?.adapter !== 'memory' && options?.adapter !== null)
|
if (options?.persistent === false && options?.adapter !== 'memory' && options?.adapter !== null)
|
||||||
throw ErrorUtils.getError('Cache.new: if not persistent, cache must use memory adapter.')
|
throw ErrorUtils.getError('Cache.new: if not persistent, cache must use memory adapter.')
|
||||||
|
|
||||||
@ -404,8 +439,12 @@ export namespace Cache {
|
|||||||
this.sync = new AsyncUtils.Sync(options?.sync)
|
this.sync = new AsyncUtils.Sync(options?.sync)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract buildDoc(response: any): Docs.Base
|
||||||
|
|
||||||
abstract getCallbacks(...args: Array<any>): Array<AsyncUtils.Callback>
|
abstract getCallbacks(...args: Array<any>): Array<AsyncUtils.Callback>
|
||||||
|
|
||||||
|
abstract getErrorHandlers(...args: Array<any>): Array<AsyncUtils.ErrorHandler>
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
if (this.sync.pooler!.pending)
|
if (this.sync.pooler!.pending)
|
||||||
throw ErrorUtils.getError("Syncable.close: can't clear while pooler still has pending promises.")
|
throw ErrorUtils.getError("Syncable.close: can't clear while pooler still has pending promises.")
|
||||||
@ -419,7 +458,40 @@ export namespace Cache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tornadoSyncErrorHandler(
|
||||||
|
err: Error,
|
||||||
|
numResolvedPromises: number,
|
||||||
|
callbackIndex: number,
|
||||||
|
orderIndex: number,
|
||||||
|
...args: any[]
|
||||||
|
): void {
|
||||||
|
err = ErrorUtils.ensureError<Error>(err)
|
||||||
|
|
||||||
|
if (err.message.match('context deadline exceeded'))
|
||||||
|
console.error(
|
||||||
|
ErrorUtils.getError(
|
||||||
|
`Context deadline exceeded, stop if more promises do not resolve. Resolved: ${numResolvedPromises}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else if (err.message.match('Invalid JSON RPC'))
|
||||||
|
console.error(
|
||||||
|
ErrorUtils.getError(`Endpoint returned invalid value (we might be rate limited), retrying.`)
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
err.message += `\nCallback args supplied: [${args.join(', ')}]\n`
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Deposit extends Syncable<Docs.Deposit> {
|
export class Deposit extends Syncable<Docs.Deposit> {
|
||||||
|
buildDoc(response: any): Docs.Deposit {
|
||||||
|
return new Docs.Deposit(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorHandlers(): Array<AsyncUtils.ErrorHandler> {
|
||||||
|
return [tornadoSyncErrorHandler]
|
||||||
|
}
|
||||||
|
|
||||||
getCallbacks(instance: TornadoInstance): Array<AsyncUtils.Callback> {
|
getCallbacks(instance: TornadoInstance): Array<AsyncUtils.Callback> {
|
||||||
return [
|
return [
|
||||||
(fromBlock: number, toBlock: number) => {
|
(fromBlock: number, toBlock: number) => {
|
||||||
@ -430,6 +502,14 @@ export namespace Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Withdrawal extends Syncable<Docs.Withdrawal> {
|
export class Withdrawal extends Syncable<Docs.Withdrawal> {
|
||||||
|
buildDoc(response: any): Docs.Withdrawal {
|
||||||
|
return new Docs.Withdrawal(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorHandlers(): Array<AsyncUtils.ErrorHandler> {
|
||||||
|
return [tornadoSyncErrorHandler]
|
||||||
|
}
|
||||||
|
|
||||||
getCallbacks(instance: TornadoInstance): Array<AsyncUtils.Callback> {
|
getCallbacks(instance: TornadoInstance): Array<AsyncUtils.Callback> {
|
||||||
return [
|
return [
|
||||||
(fromBlock: number, toBlock: number) => {
|
(fromBlock: number, toBlock: number) => {
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
import * as Crypto from 'types/sdk/crypto'
|
import * as Crypto from 'types/sdk/crypto'
|
||||||
import { Options } from 'types/sdk/core'
|
import { Options } from 'types/sdk/core'
|
||||||
|
|
||||||
// Needed
|
// External imports
|
||||||
|
import assert from 'assert'
|
||||||
import { BigNumber } from 'ethers'
|
import { BigNumber } from 'ethers'
|
||||||
import { bigInt } from 'snarkjs'
|
import { bigInt } from 'snarkjs'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
@ -22,7 +23,7 @@ export namespace ErrorUtils {
|
|||||||
|
|
||||||
export function getError(message: string): Error {
|
export function getError(message: string): Error {
|
||||||
let error = new Error(message)
|
let error = new Error(message)
|
||||||
error.name = 'Error (tornado-sdk)'
|
error.name = '\nError (tornado-sdk)'
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,25 +35,34 @@ export namespace AsyncUtils {
|
|||||||
|
|
||||||
export type Callback = (...values: any[]) => Promise<any>
|
export type Callback = (...values: any[]) => Promise<any>
|
||||||
|
|
||||||
|
export type ErrorHandler = (
|
||||||
|
err: Error,
|
||||||
|
numResolvedPromises: number,
|
||||||
|
callbackIndex: number,
|
||||||
|
orderIndex: number,
|
||||||
|
...args: any[]
|
||||||
|
) => void
|
||||||
|
|
||||||
export class PromisePooler {
|
export class PromisePooler {
|
||||||
concurrencyLimit: number
|
concurrencyLimit: number
|
||||||
private _totalAdded: number
|
private _totalAdded: number
|
||||||
|
|
||||||
_results: Array<any>
|
_results: Array<any>
|
||||||
_callbacks: Array<Callback>
|
_callbacks: Array<Callback>
|
||||||
|
_errorHandlers: Array<ErrorHandler>
|
||||||
private _promises: Array<PoolPromise<any>>
|
private _promises: Array<PoolPromise<any>>
|
||||||
|
|
||||||
constructor(callbacks: Array<Callback>, concurrencyLimit: number) {
|
constructor(callbacks: Array<Callback>, errorHandlers: Array<ErrorHandler>, concurrencyLimit: number) {
|
||||||
if (callbacks.length == 0) throw ErrorUtils.getError('PromisePooler: callbacks are empty.')
|
if (callbacks.length == 0) throw ErrorUtils.getError('PromisePooler: callbacks are empty.')
|
||||||
if (concurrencyLimit <= 0)
|
if (concurrencyLimit <= 0)
|
||||||
throw ErrorUtils.getError("PromisePooler: concurrencyLimit can't be 0 or less.")
|
throw ErrorUtils.getError("PromisePooler: concurrencyLimit can't be 0 or less.")
|
||||||
|
|
||||||
this.concurrencyLimit = concurrencyLimit
|
this.concurrencyLimit = concurrencyLimit
|
||||||
|
|
||||||
this._totalAdded = 0
|
this._totalAdded = 0
|
||||||
this._results = []
|
this._results = []
|
||||||
this._promises = []
|
this._promises = []
|
||||||
this._callbacks = callbacks
|
this._callbacks = callbacks
|
||||||
|
this._errorHandlers = errorHandlers
|
||||||
}
|
}
|
||||||
|
|
||||||
get pending(): number {
|
get pending(): number {
|
||||||
@ -98,10 +108,21 @@ export namespace AsyncUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Immediately set new callbacks and error handlers
|
||||||
async reset(): Promise<Array<any>> {
|
async reset(): Promise<Array<any>> {
|
||||||
let results = await this.all()
|
let results = await this.all()
|
||||||
this._callbacks = []
|
|
||||||
|
assert(
|
||||||
|
this._promises.length === 0,
|
||||||
|
ErrorUtils.getError('PromisePooler.reset: Resetting should have allowed all promises to resolve.')
|
||||||
|
)
|
||||||
|
|
||||||
|
this._results = []
|
||||||
this._totalAdded = 0
|
this._totalAdded = 0
|
||||||
|
|
||||||
|
this._callbacks = []
|
||||||
|
this._errorHandlers = []
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,29 +134,42 @@ export namespace AsyncUtils {
|
|||||||
return this._pool(callbackIndex + 1, orderIndex, ...results)
|
return this._pool(callbackIndex + 1, orderIndex, ...results)
|
||||||
} else {
|
} else {
|
||||||
let result = results.length == 1 ? results[0] : results
|
let result = results.length == 1 ? results[0] : results
|
||||||
this._promises.splice(
|
this._promises.splice(this._getPromiseIndex(orderIndex), 1)
|
||||||
this._promises.findIndex((_promise) => _promise.orderIndex == orderIndex),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
this._results.push(result)
|
this._results.push(result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => {
|
async (err: Error) => {
|
||||||
throw ErrorUtils.ensureError(err)
|
let resolved = this._totalAdded - this.concurrencyLimit
|
||||||
|
resolved = resolved < 0 ? 0 : resolved
|
||||||
|
// Throw inside to abort
|
||||||
|
this._errorHandlers[callbackIndex](
|
||||||
|
ErrorUtils.ensureError<Error>(err),
|
||||||
|
resolved,
|
||||||
|
callbackIndex,
|
||||||
|
orderIndex,
|
||||||
|
...args
|
||||||
|
)
|
||||||
|
return this._pool(callbackIndex, orderIndex, ...args)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
promise.orderIndex = orderIndex
|
promise.orderIndex = orderIndex
|
||||||
|
|
||||||
if (callbackIndex == 0) {
|
const promiseIndex = this._getPromiseIndex(orderIndex)
|
||||||
|
|
||||||
|
if (promiseIndex < 0) {
|
||||||
this._promises.push(promise)
|
this._promises.push(promise)
|
||||||
} else {
|
} else {
|
||||||
this._promises[this._promises.findIndex((_promise) => _promise.orderIndex == orderIndex)] = promise
|
this._promises[promiseIndex] = promise
|
||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getPromiseIndex(orderIndex: number): number {
|
||||||
|
return this._promises.findIndex((_promise) => _promise.orderIndex == orderIndex)
|
||||||
|
}
|
||||||
|
|
||||||
private async _waitIfFull(): Promise<void> {
|
private async _waitIfFull(): Promise<void> {
|
||||||
if (this.concurrencyLimit <= this._promises.length) {
|
if (this.concurrencyLimit <= this._promises.length) {
|
||||||
await Promise.race(this._promises)
|
await Promise.race(this._promises)
|
||||||
@ -153,15 +187,14 @@ export namespace AsyncUtils {
|
|||||||
this.listen = options?.listen ?? false
|
this.listen = options?.listen ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializePooler(callbacks: Array<Callback>): Promise<PromisePooler> {
|
initializePooler(callbacks: Array<Callback>, errorHandlers: Array<ErrorHandler>): void {
|
||||||
return new PromisePooler(callbacks, this.concurrencyLimit)
|
if (this.pooler) this.pooler.reset()
|
||||||
|
this.pooler = new PromisePooler(callbacks, errorHandlers, this.concurrencyLimit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async newCallbacks(callbacks: Array<Callback>): Promise<Array<any>> {
|
export function timeout(msTimeout: number): Promise<any> {
|
||||||
let results = this.pooler!.reset()
|
return new Promise((resolve) => setTimeout(resolve, msTimeout))
|
||||||
this.pooler!._callbacks = callbacks
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,9 @@ export class TorProvider extends Web3Provider {
|
|||||||
agent: { https: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) },
|
agent: { https: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) },
|
||||||
// The XHR2 XMLHttpRequest assigns a Tor Browser header by itself.
|
// The XHR2 XMLHttpRequest assigns a Tor Browser header by itself.
|
||||||
// But if in Browser we assign just in case.
|
// But if in Browser we assign just in case.
|
||||||
headers: typeof window !== 'undefined' ? headers : undefined
|
headers: typeof window !== 'undefined' ? headers : undefined,
|
||||||
|
// 1 minute timeout, although not sure if this is behaving properly
|
||||||
|
timeout: 60000
|
||||||
}),
|
}),
|
||||||
network
|
network
|
||||||
)
|
)
|
||||||
@ -135,10 +137,12 @@ export class Relayer {
|
|||||||
this._serviceFee = properties['tornadoServiceFee']
|
this._serviceFee = properties['tornadoServiceFee']
|
||||||
this._miningFee = properties['miningFee']
|
this._miningFee = properties['miningFee']
|
||||||
this._status = properties['health']['status']
|
this._status = properties['health']['status']
|
||||||
|
|
||||||
this._prices = Object.entries(properties['ethPrices']).reduce(
|
this._prices = Object.entries(properties['ethPrices']).reduce(
|
||||||
(map, entry) => map.set(entry[0], BigNumber.from(entry[1])),
|
(map, entry) => map.set(entry[0], BigNumber.from(entry[1])),
|
||||||
new Map<string, BigNumber>()
|
new Map<string, BigNumber>()
|
||||||
)
|
)
|
||||||
|
|
||||||
this._fetched = true
|
this._fetched = true
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
1
src/resources/onchain/decimals.json
Normal file
1
src/resources/onchain/decimals.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"1eth":18,"1dai":18,"1cdai":8,"1usdc":6,"1usdt":6,"1wbtc":8,"5eth":18,"5dai":18,"5cdai":8,"5usdc":6,"5usdt":6,"5wbtc":8,"10eth":18,"56bnb":18,"100xdai":18,"137matic":18,"42161eth":18,"43114avax":18}
|
66
src/resources/onchain/deployedBlockNumbers.json
Normal file
66
src/resources/onchain/deployedBlockNumbers.json
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"1eth0.1": 9116966,
|
||||||
|
"1eth1": 9117609,
|
||||||
|
"1eth10": 9117720,
|
||||||
|
"1eth100": 9161895,
|
||||||
|
"1dai100": 9117612,
|
||||||
|
"1dai1000": 9161917,
|
||||||
|
"1dai10000": 12066007,
|
||||||
|
"1dai100000": 12066048,
|
||||||
|
"1cdai5000": 9161938,
|
||||||
|
"1cdai50000": 12069037,
|
||||||
|
"1cdai500000": 12067606,
|
||||||
|
"1cdai5000000": 12066053,
|
||||||
|
"1usdc100": 9161958,
|
||||||
|
"1usdc1000": 9161965,
|
||||||
|
"1usdc10000": null,
|
||||||
|
"1usdc100000": null,
|
||||||
|
"1usdt100": 9162005,
|
||||||
|
"1usdt1000": 9162012,
|
||||||
|
"1usdt10000": null,
|
||||||
|
"1usdt100000": null,
|
||||||
|
"1wbtc0.1": 12067529,
|
||||||
|
"1wbtc1": 12066652,
|
||||||
|
"1wbtc10": 12067591,
|
||||||
|
"1wbtc100": null,
|
||||||
|
"5eth0.1": 3782581,
|
||||||
|
"5eth1": 3782590,
|
||||||
|
"5eth10": 3782593,
|
||||||
|
"5eth100": 3782596,
|
||||||
|
"5dai100": 4339088,
|
||||||
|
"5dai1000": 4367659,
|
||||||
|
"5dai10000": 4441492,
|
||||||
|
"5dai100000": 4441488,
|
||||||
|
"5cdai5000": 4441443,
|
||||||
|
"5cdai50000": 4441489,
|
||||||
|
"5cdai500000": 4441493,
|
||||||
|
"5cdai5000000": 4441489,
|
||||||
|
"5usdc100": 4441426,
|
||||||
|
"5usdc1000": 4441492,
|
||||||
|
"5usdc10000": null,
|
||||||
|
"5usdc100000": null,
|
||||||
|
"5usdt100": 4441490,
|
||||||
|
"5usdt1000": 4441492,
|
||||||
|
"5usdt10000": null,
|
||||||
|
"5usdt100000": null,
|
||||||
|
"5wbtc0.1": 4441488,
|
||||||
|
"5wbtc1": 4441490,
|
||||||
|
"5wbtc10": 4441490,
|
||||||
|
"5wbtc100": null,
|
||||||
|
"56bnb0.1": 8159279,
|
||||||
|
"56bnb1": 8159286,
|
||||||
|
"56bnb10": 8159290,
|
||||||
|
"56bnb100": 8159296,
|
||||||
|
"100xdai100": 17754566,
|
||||||
|
"100xdai1000": 17754568,
|
||||||
|
"100xdai10000": 17754572,
|
||||||
|
"100xdai100000": 17754574,
|
||||||
|
"137matic100": 16258013,
|
||||||
|
"137matic1000": 16258032,
|
||||||
|
"137matic10000": 16258046,
|
||||||
|
"137matic100000": 16258053,
|
||||||
|
"42161eth0.1": 3300000,
|
||||||
|
"42161eth1": 3300000,
|
||||||
|
"42161eth10": 3300000,
|
||||||
|
"42161eth100": 3300000
|
||||||
|
}
|
66
src/resources/onchain/instanceAddresses.json
Normal file
66
src/resources/onchain/instanceAddresses.json
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"1eth0.1": "0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc",
|
||||||
|
"1eth1": "0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936",
|
||||||
|
"1eth10": "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF",
|
||||||
|
"1eth100": "0xA160cdAB225685dA1d56aa342Ad8841c3b53f291",
|
||||||
|
"1dai100": "0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3",
|
||||||
|
"1dai1000": "0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144",
|
||||||
|
"1dai10000": "0x07687e702b410Fa43f4cB4Af7FA097918ffD2730",
|
||||||
|
"1dai100000": "0x23773E65ed146A459791799d01336DB287f25334",
|
||||||
|
"1cdai5000": "0x22aaA7720ddd5388A3c0A3333430953C68f1849b",
|
||||||
|
"1cdai50000": "0x03893a7c7463AE47D46bc7f091665f1893656003",
|
||||||
|
"1cdai500000": "0x2717c5e28cf931547B621a5dddb772Ab6A35B701",
|
||||||
|
"1cdai5000000": "0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af",
|
||||||
|
"1usdc100": "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307",
|
||||||
|
"1usdc1000": "0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D",
|
||||||
|
"1usdc10000": null,
|
||||||
|
"1usdc100000": null,
|
||||||
|
"1usdt100": "0x169AD27A470D064DEDE56a2D3ff727986b15D52B",
|
||||||
|
"1usdt1000": "0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f",
|
||||||
|
"1usdt10000": null,
|
||||||
|
"1usdt100000": null,
|
||||||
|
"1wbtc0.1": "0x178169B423a011fff22B9e3F3abeA13414dDD0F1",
|
||||||
|
"1wbtc1": "0x610B717796ad172B316836AC95a2ffad065CeaB4",
|
||||||
|
"1wbtc10": "0xbB93e510BbCD0B7beb5A853875f9eC60275CF498",
|
||||||
|
"1wbtc100": null,
|
||||||
|
"5eth0.1": "0x6Bf694a291DF3FeC1f7e69701E3ab6c592435Ae7",
|
||||||
|
"5eth1": "0x3aac1cC67c2ec5Db4eA850957b967Ba153aD6279",
|
||||||
|
"5eth10": "0x723B78e67497E85279CB204544566F4dC5d2acA0",
|
||||||
|
"5eth100": "0x0E3A09dDA6B20aFbB34aC7cD4A6881493f3E7bf7",
|
||||||
|
"5dai100": "0x76D85B4C0Fc497EeCc38902397aC608000A06607",
|
||||||
|
"5dai1000": "0xCC84179FFD19A1627E79F8648d09e095252Bc418",
|
||||||
|
"5dai10000": "0xD5d6f8D9e784d0e26222ad3834500801a68D027D",
|
||||||
|
"5dai100000": "0x407CcEeaA7c95d2FE2250Bf9F2c105aA7AAFB512",
|
||||||
|
"5cdai5000": "0x833481186f16Cece3f1Eeea1a694c42034c3a0dB",
|
||||||
|
"5cdai50000": "0xd8D7DE3349ccaA0Fde6298fe6D7b7d0d34586193",
|
||||||
|
"5cdai500000": "0x8281Aa6795aDE17C8973e1aedcA380258Bc124F9",
|
||||||
|
"5cdai5000000": "0x57b2B8c82F065de8Ef5573f9730fC1449B403C9f",
|
||||||
|
"5usdc100": "0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45",
|
||||||
|
"5usdc1000": "0x23173fE8b96A4Ad8d2E17fB83EA5dcccdCa1Ae52",
|
||||||
|
"5usdc10000": null,
|
||||||
|
"5usdc100000": null,
|
||||||
|
"5usdt100": "0x538Ab61E8A9fc1b2f93b3dd9011d662d89bE6FE6",
|
||||||
|
"5usdt1000": "0x94Be88213a387E992Dd87DE56950a9aef34b9448",
|
||||||
|
"5usdt10000": null,
|
||||||
|
"5usdt100000": null,
|
||||||
|
"5wbtc0.1": "0x242654336ca2205714071898f67E254EB49ACdCe",
|
||||||
|
"5wbtc1": "0x776198CCF446DFa168347089d7338879273172cF",
|
||||||
|
"5wbtc10": "0xeDC5d01286f99A066559F60a585406f3878a033e",
|
||||||
|
"5wbtc100": null,
|
||||||
|
"56bnb0.1": "0x84443CFd09A48AF6eF360C6976C5392aC5023a1F",
|
||||||
|
"56bnb1": "0xd47438C816c9E7f2E2888E060936a499Af9582b3",
|
||||||
|
"56bnb10": "0x330bdFADE01eE9bF63C209Ee33102DD334618e0a",
|
||||||
|
"56bnb100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD",
|
||||||
|
"100xdai100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD",
|
||||||
|
"100xdai1000": "0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178",
|
||||||
|
"100xdai10000": "0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040",
|
||||||
|
"100xdai100000": "0xa5C2254e4253490C54cef0a4347fddb8f75A4998",
|
||||||
|
"137matic100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD",
|
||||||
|
"137matic1000": "0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178",
|
||||||
|
"137matic10000": "0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040",
|
||||||
|
"137matic100000": "0xa5C2254e4253490C54cef0a4347fddb8f75A4998",
|
||||||
|
"42161eth0.1": "0x84443CFd09A48AF6eF360C6976C5392aC5023a1F",
|
||||||
|
"42161eth1": "0xd47438C816c9E7f2E2888E060936a499Af9582b3",
|
||||||
|
"42161eth10": "0x330bdFADE01eE9bF63C209Ee33102DD334618e0a",
|
||||||
|
"42161eth100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD"
|
||||||
|
}
|
8
src/resources/onchain/networkSymbols.json
Normal file
8
src/resources/onchain/networkSymbols.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"1": "eth",
|
||||||
|
"5": "eth",
|
||||||
|
"56": "bnb",
|
||||||
|
"100": "xdai",
|
||||||
|
"137": "matic",
|
||||||
|
"42161": "eth"
|
||||||
|
}
|
@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"instanceAddresses": {
|
|
||||||
"1eth0.1": "0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc",
|
|
||||||
"1eth1": "0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936",
|
|
||||||
"1eth10": "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF",
|
|
||||||
"1eth100": "0xA160cdAB225685dA1d56aa342Ad8841c3b53f291",
|
|
||||||
"1dai100": "0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3",
|
|
||||||
"1dai1000": "0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144",
|
|
||||||
"1dai10000": "0x07687e702b410Fa43f4cB4Af7FA097918ffD2730",
|
|
||||||
"1dai100000": "0x23773E65ed146A459791799d01336DB287f25334",
|
|
||||||
"1cdai5000": "0x22aaA7720ddd5388A3c0A3333430953C68f1849b",
|
|
||||||
"1cdai50000": "0x03893a7c7463AE47D46bc7f091665f1893656003",
|
|
||||||
"1cdai500000": "0x2717c5e28cf931547B621a5dddb772Ab6A35B701",
|
|
||||||
"1cdai5000000": "0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af",
|
|
||||||
"1usdc100": "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307",
|
|
||||||
"1usdc1000": "0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D",
|
|
||||||
"1usdc10000": null,
|
|
||||||
"1usdc100000": null,
|
|
||||||
"1usdt100": "0x169AD27A470D064DEDE56a2D3ff727986b15D52B",
|
|
||||||
"1usdt1000": "0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f",
|
|
||||||
"1usdt10000": null,
|
|
||||||
"1usdt100000": null,
|
|
||||||
"1wbtc0.1": "0x178169B423a011fff22B9e3F3abeA13414dDD0F1",
|
|
||||||
"1wbtc1": "0x610B717796ad172B316836AC95a2ffad065CeaB4",
|
|
||||||
"1wbtc10": "0xbB93e510BbCD0B7beb5A853875f9eC60275CF498",
|
|
||||||
"1wbtc100": null,
|
|
||||||
"5eth0.1": "0x6Bf694a291DF3FeC1f7e69701E3ab6c592435Ae7",
|
|
||||||
"5eth1": "0x3aac1cC67c2ec5Db4eA850957b967Ba153aD6279",
|
|
||||||
"5eth10": "0x723B78e67497E85279CB204544566F4dC5d2acA0",
|
|
||||||
"5eth100": "0x0E3A09dDA6B20aFbB34aC7cD4A6881493f3E7bf7",
|
|
||||||
"5dai100": "0x76D85B4C0Fc497EeCc38902397aC608000A06607",
|
|
||||||
"5dai1000": "0xCC84179FFD19A1627E79F8648d09e095252Bc418",
|
|
||||||
"5dai10000": "0xD5d6f8D9e784d0e26222ad3834500801a68D027D",
|
|
||||||
"5dai100000": "0x407CcEeaA7c95d2FE2250Bf9F2c105aA7AAFB512",
|
|
||||||
"5cdai5000": "0x833481186f16Cece3f1Eeea1a694c42034c3a0dB",
|
|
||||||
"5cdai50000": "0xd8D7DE3349ccaA0Fde6298fe6D7b7d0d34586193",
|
|
||||||
"5cdai500000": "0x8281Aa6795aDE17C8973e1aedcA380258Bc124F9",
|
|
||||||
"5cdai5000000": "0x57b2B8c82F065de8Ef5573f9730fC1449B403C9f",
|
|
||||||
"5usdc100": "0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45",
|
|
||||||
"5usdc1000": "0x23173fE8b96A4Ad8d2E17fB83EA5dcccdCa1Ae52",
|
|
||||||
"5usdc10000": null,
|
|
||||||
"5usdc100000": null,
|
|
||||||
"5usdt100": "0x538Ab61E8A9fc1b2f93b3dd9011d662d89bE6FE6",
|
|
||||||
"5usdt1000": "0x94Be88213a387E992Dd87DE56950a9aef34b9448",
|
|
||||||
"5usdt10000": null,
|
|
||||||
"5usdt100000": null,
|
|
||||||
"5wbtc0.1": "0x242654336ca2205714071898f67E254EB49ACdCe",
|
|
||||||
"5wbtc1": "0x776198CCF446DFa168347089d7338879273172cF",
|
|
||||||
"5wbtc10": "0xeDC5d01286f99A066559F60a585406f3878a033e",
|
|
||||||
"5wbtc100": null,
|
|
||||||
"56bnb0.1": "0x84443CFd09A48AF6eF360C6976C5392aC5023a1F",
|
|
||||||
"56bnb1": "0xd47438C816c9E7f2E2888E060936a499Af9582b3",
|
|
||||||
"56bnb10": "0x330bdFADE01eE9bF63C209Ee33102DD334618e0a",
|
|
||||||
"56bnb100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD",
|
|
||||||
"100xdai100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD",
|
|
||||||
"100xdai1000": "0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178",
|
|
||||||
"100xdai10000": "0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040",
|
|
||||||
"100xdai100000": "0xa5C2254e4253490C54cef0a4347fddb8f75A4998",
|
|
||||||
"137matic100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD",
|
|
||||||
"137matic1000": "0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178",
|
|
||||||
"137matic10000": "0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040",
|
|
||||||
"137matic100000": "0xa5C2254e4253490C54cef0a4347fddb8f75A4998",
|
|
||||||
"42161eth0.1": "0x84443CFd09A48AF6eF360C6976C5392aC5023a1F",
|
|
||||||
"42161eth1": "0xd47438C816c9E7f2E2888E060936a499Af9582b3",
|
|
||||||
"42161eth10": "0x330bdFADE01eE9bF63C209Ee33102DD334618e0a",
|
|
||||||
"42161eth100": "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD"
|
|
||||||
},
|
|
||||||
"deployedBlockNumber": {
|
|
||||||
"1eth0.1": 9116966,
|
|
||||||
"1eth1": 9117609,
|
|
||||||
"1eth10": 9117720,
|
|
||||||
"1eth100": 9161895,
|
|
||||||
"1dai100": 9117612,
|
|
||||||
"1dai1000": 9161917,
|
|
||||||
"1dai10000": 12066007,
|
|
||||||
"1dai100000": 12066048,
|
|
||||||
"1cdai5000": 9161938,
|
|
||||||
"1cdai50000": 12069037,
|
|
||||||
"1cdai500000": 12067606,
|
|
||||||
"1cdai5000000": 12066053,
|
|
||||||
"1usdc100": 9161958,
|
|
||||||
"1usdc1000": 9161965,
|
|
||||||
"1usdc10000": null,
|
|
||||||
"1usdc100000": null,
|
|
||||||
"1usdt100": 9162005,
|
|
||||||
"1usdt1000": 9162012,
|
|
||||||
"1usdt10000": null,
|
|
||||||
"1usdt100000": null,
|
|
||||||
"1wbtc0.1": 12067529,
|
|
||||||
"1wbtc1": 12066652,
|
|
||||||
"1wbtc10": 12067591,
|
|
||||||
"1wbtc100": null,
|
|
||||||
"5eth0.1": 3782581,
|
|
||||||
"5eth1": 3782590,
|
|
||||||
"5eth10": 3782593,
|
|
||||||
"5eth100": 3782596,
|
|
||||||
"5dai100": 4339088,
|
|
||||||
"5dai1000": 4367659,
|
|
||||||
"5dai10000": 4441492,
|
|
||||||
"5dai100000": 4441488,
|
|
||||||
"5cdai5000": 4441443,
|
|
||||||
"5cdai50000": 4441489,
|
|
||||||
"5cdai500000": 4441493,
|
|
||||||
"5cdai5000000": 4441489,
|
|
||||||
"5usdc100": 4441426,
|
|
||||||
"5usdc1000": 4441492,
|
|
||||||
"5usdc10000": null,
|
|
||||||
"5usdc100000": null,
|
|
||||||
"5usdt100": 4441490,
|
|
||||||
"5usdt1000": 4441492,
|
|
||||||
"5usdt10000": null,
|
|
||||||
"5usdt100000": null,
|
|
||||||
"5wbtc0.1": 4441488,
|
|
||||||
"5wbtc1": 4441490,
|
|
||||||
"5wbtc10": 4441490,
|
|
||||||
"5wbtc100": null,
|
|
||||||
"56bnb0.1": 8159279,
|
|
||||||
"56bnb1": 8159286,
|
|
||||||
"56bnb10": 8159290,
|
|
||||||
"56bnb100": 8159296,
|
|
||||||
"100xdai100": 17754566,
|
|
||||||
"100xdai1000": 17754568,
|
|
||||||
"100xdai10000": 17754572,
|
|
||||||
"100xdai100000": 17754574,
|
|
||||||
"137matic100": 16258013,
|
|
||||||
"137matic1000": 16258032,
|
|
||||||
"137matic10000": 16258046,
|
|
||||||
"137matic100000": 16258053,
|
|
||||||
"42161eth0.1": 3300000,
|
|
||||||
"42161eth1": 3300000,
|
|
||||||
"42161eth10": 3300000,
|
|
||||||
"42161eth100": 3300000
|
|
||||||
},
|
|
||||||
"networkSymbols": {
|
|
||||||
"1": "eth",
|
|
||||||
"5": "eth",
|
|
||||||
"56": "bnb",
|
|
||||||
"100": "xdai",
|
|
||||||
"137": "matic",
|
|
||||||
"42161": "eth"
|
|
||||||
}
|
|
||||||
}
|
|
1
src/resources/onchain/tokenAddresses.json
Normal file
1
src/resources/onchain/tokenAddresses.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"1dai":"0x6B175474E89094C44Da98b954EedeAC495271d0F","1cdai":"0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643","1usdc":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","1usdt":"0xdAC17F958D2ee523a2206206994597C13D831ec7","1wbtc":"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599","5dai":"0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60","5cdai":"0x822397d9a55d0fefd20F5c4bCaB33C5F65bd28Eb","5usdc":"0xD87Ba7A50B2E7E660f678A895E4B72E7CB4CCd9C","5usdt":"0xb7FC2023D96AEa94Ba0254AA5Aeb93141e4aad66","5wbtc":"0xC04B0d3107736C32e19F1c62b2aF67BE61d63a05"}
|
@ -9,7 +9,7 @@ import { ERC20, TornadoInstance } from 'types/deth'
|
|||||||
import { Json } from 'types/sdk/data'
|
import { Json } from 'types/sdk/data'
|
||||||
import { Core } from 'lib/core'
|
import { Core } from 'lib/core'
|
||||||
import { Chain, Contracts } from 'lib/chain'
|
import { Chain, Contracts } from 'lib/chain'
|
||||||
import { Docs, Files, OnchainData, Cache } from 'lib/data'
|
import { Docs, Files, Onchain, Cache } from 'lib/data'
|
||||||
import { ErrorUtils } from 'lib/utils'
|
import { ErrorUtils } from 'lib/utils'
|
||||||
import { TorProvider } from 'lib/web'
|
import { TorProvider } from 'lib/web'
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ chai.use(solidity)
|
|||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe.only('main', () => {
|
describe('main', () => {
|
||||||
const torify = process.env.TORIFY === 'true'
|
const torify = process.env.TORIFY === 'true'
|
||||||
|
|
||||||
if (!process.env.ETH_MAINNET_TEST_RPC) throw ErrorUtils.getError('need a mainnet rpc endpoint.')
|
if (!process.env.ETH_MAINNET_TEST_RPC) throw ErrorUtils.getError('need a mainnet rpc endpoint.')
|
||||||
@ -34,11 +34,11 @@ describe.only('main', () => {
|
|||||||
'Also, we are using ganache because we just need a forked blockchain and not an entire environment. 🐧'
|
'Also, we are using ganache because we just need a forked blockchain and not an entire environment. 🐧'
|
||||||
)
|
)
|
||||||
|
|
||||||
let daiData: Json.TokenData
|
let daiAddress: string
|
||||||
const daiWhale = '0x5777d92f208679db4b9778590fa3cab3ac9e2168' // Uniswap V3 Something/Dai Pool
|
const daiWhale = '0x5777d92f208679db4b9778590fa3cab3ac9e2168' // Uniswap V3 Something/Dai Pool
|
||||||
|
|
||||||
const mainnetProvider = torify
|
const mainnetProvider: providers.Provider = torify
|
||||||
? new TorProvider(process.env.ETH_MAINNET_TEST_RPC, { port: +process.env.TOR_PORT! })
|
? new TorProvider(process.env.ETH_MAINNET_TEST_RPC!, { port: +process.env.TOR_PORT! })
|
||||||
: new providers.JsonRpcProvider(process.env.ETH_MAINNET_TEST_RPC)
|
: new providers.JsonRpcProvider(process.env.ETH_MAINNET_TEST_RPC)
|
||||||
|
|
||||||
const _ganacheProvider = ganache.provider({
|
const _ganacheProvider = ganache.provider({
|
||||||
@ -54,6 +54,7 @@ describe.only('main', () => {
|
|||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const ganacheProvider = new providers.Web3Provider(_ganacheProvider)
|
const ganacheProvider = new providers.Web3Provider(_ganacheProvider)
|
||||||
|
|
||||||
const chain = new Chain(ganacheProvider)
|
const chain = new Chain(ganacheProvider)
|
||||||
|
|
||||||
let snapshotId: any
|
let snapshotId: any
|
||||||
@ -64,7 +65,7 @@ describe.only('main', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('namespace Tornado', () => {
|
describe('namespace Tornado', () => {
|
||||||
describe.skip('namespace Contracts', () => {
|
describe('namespace Contracts', () => {
|
||||||
it('getClassicInstance: should be able to get a tornado instance', async () => {
|
it('getClassicInstance: should be able to get a tornado instance', async () => {
|
||||||
let instance = await Contracts.getInstance(String(1), 'eth', String(1), mainnetProvider)
|
let instance = await Contracts.getInstance(String(1), 'eth', String(1), mainnetProvider)
|
||||||
expect(instance.address).to.equal('0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936')
|
expect(instance.address).to.equal('0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936')
|
||||||
@ -73,21 +74,30 @@ describe.only('main', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('class Classic', () => {
|
describe('class Classic', () => {
|
||||||
it.skip('sync: Should be able to fetch deposit events', async () => {
|
it.only('sync: Should be able to fetch deposit events', async function () {
|
||||||
const core = new Core(mainnetProvider)
|
const core = new Core(mainnetProvider)
|
||||||
const instance = await Contracts.getInstance(String(1), 'eth', String(0.1), mainnetProvider)
|
const instance = await Contracts.getInstance(String(1), 'eth', String(0.1), mainnetProvider)
|
||||||
//const targetBlock = 16928712
|
|
||||||
//const startBlock = targetBlock - 7200
|
// For safety
|
||||||
|
expect(torify).to.be.true
|
||||||
|
|
||||||
|
core.on('debug', function (...args) {
|
||||||
|
if (args.length === 3) {
|
||||||
|
console.debug(`\nSync will be started with SB: ${args[0]}, TB: ${args[1]}, BD: ${args[2]}\n`)
|
||||||
|
} else if (args.length == 2) {
|
||||||
|
console.debug(`Syncing from block ${args[0]} to ${args[1]}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// This is going to try syncing the entire range
|
||||||
|
|
||||||
await core.sync(instance, {
|
await core.sync(instance, {
|
||||||
//deposit: true,
|
|
||||||
//withdrawal: false,
|
|
||||||
blocks: {
|
blocks: {
|
||||||
//startBlock: startBlock,
|
deltaDivisor: 50
|
||||||
//targetBlock: targetBlock
|
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
sync: {
|
sync: {
|
||||||
concurrencyLimit: 10
|
concurrencyLimit: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -124,8 +134,8 @@ describe.only('main', () => {
|
|||||||
await ganacheProvider.send('evm_setAccountBalance', [daiWhale, parseUnits('10').toHexString()])
|
await ganacheProvider.send('evm_setAccountBalance', [daiWhale, parseUnits('10').toHexString()])
|
||||||
|
|
||||||
needsMoneyAddress = await needsMoney.getAddress()
|
needsMoneyAddress = await needsMoney.getAddress()
|
||||||
daiData = await OnchainData.getTokenData('1', 'dai')
|
daiAddress = await Onchain.getTokenAddress('1', 'dai')
|
||||||
dai = chain.getTokenContract(daiData.address).connect(whale)
|
dai = chain.getTokenContract(daiAddress).connect(whale)
|
||||||
smallestEth = await core.getInstance('eth', 0.1)
|
smallestEth = await core.getInstance('eth', 0.1)
|
||||||
})
|
})
|
||||||
after(async function () {
|
after(async function () {
|
||||||
@ -137,11 +147,11 @@ describe.only('main', () => {
|
|||||||
dai = dai.connect(whale)
|
dai = dai.connect(whale)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.only('buildDepositTx: build a single eth deposit tx and succeed', async () => {
|
it('buildDepositTransaction: build a single eth deposit tx and succeed', async () => {
|
||||||
const signer = ganacheProvider.getSigner()
|
const signer = ganacheProvider.getSigner()
|
||||||
const initBal = await signer.getBalance()
|
const initBal = await signer.getBalance()
|
||||||
|
|
||||||
const tx = await core.buildDepositTx(smallestEth)
|
const tx = await core.buildDepositTransaction(smallestEth)
|
||||||
const response = await signer.sendTransaction(tx.request)
|
const response = await signer.sendTransaction(tx.request)
|
||||||
const receipt = await response.wait()
|
const receipt = await response.wait()
|
||||||
|
|
||||||
@ -165,7 +175,7 @@ describe.only('main', () => {
|
|||||||
expect(endBal).to.be.lte(parseUnits('999.9'))
|
expect(endBal).to.be.lte(parseUnits('999.9'))
|
||||||
}).timeout(0)
|
}).timeout(0)
|
||||||
|
|
||||||
it.only('buildDepositProofs: it should be able to build', async () => {
|
it('buildDepositProofs: it should be able to build', async () => {
|
||||||
try {
|
try {
|
||||||
const instance = await core.getInstance('eth', 0.1)
|
const instance = await core.getInstance('eth', 0.1)
|
||||||
const signer = ganacheProvider.getSigner()
|
const signer = ganacheProvider.getSigner()
|
||||||
@ -175,8 +185,9 @@ describe.only('main', () => {
|
|||||||
|
|
||||||
noteObj['args'] = {
|
noteObj['args'] = {
|
||||||
commitment: note.hexCommitment,
|
commitment: note.hexCommitment,
|
||||||
leafIndex: (await cache!.db.allDocs({ descending: true, limit: 1, include_docs: true }))
|
leafIndex:
|
||||||
?.rows[0].doc?.leafIndex,
|
(await cache!.db.allDocs({ descending: true, limit: 1, include_docs: true }))?.rows[0].doc
|
||||||
|
?.leafIndex! + 1,
|
||||||
timestamp: noteObj['args']['timestamp']
|
timestamp: noteObj['args']['timestamp']
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,15 +195,16 @@ describe.only('main', () => {
|
|||||||
|
|
||||||
await cache!.db.put(new Docs.Deposit(noteObj))
|
await cache!.db.put(new Docs.Deposit(noteObj))
|
||||||
|
|
||||||
console.log(`\nBuilding proof from note:\n\n${note}\n\n`)
|
|
||||||
|
|
||||||
const proof = await core.buildDepositProof(
|
const proof = await core.buildDepositProof(
|
||||||
instance,
|
instance,
|
||||||
{
|
{
|
||||||
address: await withdrawer.getAddress()
|
address: await withdrawer.getAddress()
|
||||||
},
|
},
|
||||||
await signer.getAddress(),
|
await signer.getAddress(),
|
||||||
note
|
note,
|
||||||
|
{
|
||||||
|
checkNotesSpent: false
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(proof)
|
console.log(proof)
|
||||||
@ -202,7 +214,7 @@ describe.only('main', () => {
|
|||||||
}
|
}
|
||||||
}).timeout(0)
|
}).timeout(0)
|
||||||
|
|
||||||
it.skip('buildDepositTx: build a single token deposit tx and succeed', async () => {
|
it('buildDepositTransaction: build a single token deposit tx and succeed', async () => {
|
||||||
const dai100K = await core.getInstance('dai', 100000)
|
const dai100K = await core.getInstance('dai', 100000)
|
||||||
const proxy = await core.getProxy()
|
const proxy = await core.getProxy()
|
||||||
const depositAmount = parseUnits('100000')
|
const depositAmount = parseUnits('100000')
|
||||||
@ -210,7 +222,7 @@ describe.only('main', () => {
|
|||||||
await dai.transfer(needsMoneyAddress, depositAmount)
|
await dai.transfer(needsMoneyAddress, depositAmount)
|
||||||
dai = dai.connect(needsMoney)
|
dai = dai.connect(needsMoney)
|
||||||
|
|
||||||
const tx = await core.buildDepositTx(dai100K)
|
const tx = await core.buildDepositTransaction(dai100K)
|
||||||
|
|
||||||
await dai.approve(proxy.address, depositAmount)
|
await dai.approve(proxy.address, depositAmount)
|
||||||
|
|
||||||
@ -219,13 +231,13 @@ describe.only('main', () => {
|
|||||||
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
||||||
}).timeout(0)
|
}).timeout(0)
|
||||||
|
|
||||||
it.skip('buildDepositTxs: multiple eth deposits', async () => {
|
it('buildDepositTransactions: multiple eth deposits', async () => {
|
||||||
const instances = await core.getInstances(
|
const instances = await core.getInstances(
|
||||||
[0.1, 1, 10, 100].map((el) => {
|
[0.1, 1, 10, 100].map((el) => {
|
||||||
return { token: 'eth', denomination: el }
|
return { token: 'eth', denomination: el }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const txs = await core.buildDepositTxs(instances, {
|
const txs = await core.buildDepositTransactions(instances, {
|
||||||
depositsPerInstance: [1, 2, 3, 4]
|
depositsPerInstance: [1, 2, 3, 4]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -236,7 +248,7 @@ describe.only('main', () => {
|
|||||||
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
||||||
}).timeout(0)
|
}).timeout(0)
|
||||||
|
|
||||||
it.skip('buildDepositTxs: multiple token deposits', async () => {
|
it('buildDepositTransactions: multiple token deposits', async () => {
|
||||||
const instances = await core.getInstances(
|
const instances = await core.getInstances(
|
||||||
[100, 1000, 10000, 100000].map((el) => {
|
[100, 1000, 10000, 100000].map((el) => {
|
||||||
return { token: 'dai', denomination: el }
|
return { token: 'dai', denomination: el }
|
||||||
@ -249,7 +261,7 @@ describe.only('main', () => {
|
|||||||
await dai.transfer(needsMoneyAddress, parseUnits('432100'))
|
await dai.transfer(needsMoneyAddress, parseUnits('432100'))
|
||||||
dai = dai.connect(needsMoney)
|
dai = dai.connect(needsMoney)
|
||||||
|
|
||||||
const txs = await core.buildDepositTxs(instances, {
|
const txs = await core.buildDepositTransactions(instances, {
|
||||||
depositsPerInstance: [1, 2, 3, 4]
|
depositsPerInstance: [1, 2, 3, 4]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -262,7 +274,7 @@ describe.only('main', () => {
|
|||||||
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
||||||
}).timeout(0)
|
}).timeout(0)
|
||||||
|
|
||||||
it.skip('createInvoice: should be able to create an invoice', async () => {
|
it('createInvoice: should be able to create an invoice', async () => {
|
||||||
const instance = await core.getInstance('dai', '1000')
|
const instance = await core.getInstance('dai', '1000')
|
||||||
const invoice = await core.createInvoice(instance)
|
const invoice = await core.createInvoice(instance)
|
||||||
console.log(invoice)
|
console.log(invoice)
|
||||||
|
@ -21,18 +21,22 @@ export namespace Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace Core {
|
export namespace Core {
|
||||||
|
export interface Cache {
|
||||||
|
sync?: Cache.Sync
|
||||||
|
db?: Cache.Database
|
||||||
|
}
|
||||||
|
|
||||||
export interface Sync {
|
export interface Sync {
|
||||||
deposit?: boolean
|
deposit?: boolean
|
||||||
withdrawal?: boolean
|
withdrawal?: boolean
|
||||||
|
msTimeout?: number
|
||||||
blocks?: {
|
blocks?: {
|
||||||
startBlock?: number
|
startBlock?: number
|
||||||
targetBlock?: number
|
targetBlock?: number
|
||||||
blockDelta?: number
|
blockDelta?: number
|
||||||
|
deltaDivisor?: number
|
||||||
}
|
}
|
||||||
cache?: {
|
cache?: Cache
|
||||||
sync?: Cache.Sync
|
|
||||||
db?: Cache.Database
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Deposit {
|
export interface Deposit {
|
||||||
|
Loading…
Reference in New Issue
Block a user