Compare commits

...

18 Commits

Author SHA1 Message Date
65b670d0dd
Tornado CLI 1.0.3-alpha
Fixed withdraw command from cli.js
2024-04-29 05:08:35 +00:00
5dc2b230ba
Rebuilt cli.js 2024-04-29 05:07:26 +00:00
8a0bd4a0d6
Build cli.js from webpack instead of rollup.js 2024-04-29 05:04:44 +00:00
a5b87ccf74
Tornado CLI 1.0.2-alpha 2024-04-29 04:17:49 +00:00
867a45717e
Synced events 2024-04-29 03:28:11 +00:00
b1d3155ebb
Fixed bug on NetId iteration 2024-04-29 01:04:52 +00:00
7dcd41c2c3
Built files 2024-04-29 00:36:26 +00:00
0d8c3cc7a4
Use NetIdType
idea by @Theo
2024-04-29 00:34:16 +00:00
5eb3a310c5
Added Governance Subgraph 2024-04-28 22:08:48 +00:00
8f656af0ac
Add offchain oracle contract 2024-04-27 23:16:04 +00:00
28508ef299
Improve networkConfig 2024-04-27 19:52:03 +00:00
a2ea239ea8
Cached tree generation 2024-04-27 13:55:15 +00:00
183dc5ca60
Improve networkConfig and added more relayer data 2024-04-26 22:22:46 +00:00
3312f44a4d
Save encrypted notes on router 2024-04-26 19:32:49 +00:00
195da66ce2
Added Encrypted Note Service 2024-04-26 13:16:00 +00:00
3f55d5ca99
Added Echo Service 2024-04-25 21:02:27 +00:00
bf6f5f21ae
Update built files 2024-04-25 11:23:12 +00:00
f8567d895f
Use webpack to build umd files and fix fflate imports for node.js 2024-04-25 10:51:42 +00:00
62 changed files with 555984 additions and 178842 deletions

View File

@ -42,10 +42,12 @@ module.exports = {
}
],
"import/order": ["error"],
/**
"indent": [
"error",
2
],
**/
"linebreak-style": [
"error",
"unix"

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ node_modules
/events
/trees
backup-tornado-*
backup-tornadoInvoice-*
backup-tornadoInvoice-*
backup-note-account-*

View File

@ -6,7 +6,7 @@
Modern Toolsets for [Privacy Pools](https://www.forbes.com/sites/tomerniv/2023/09/07/privacy-pools-bridging-the-gap-between-blockchain-and-regulatory-compliance) on Ethereum
[![Telegram Badge](https://img.shields.io/badge/Join%20Group-telegram?style=flat&logo=telegram&color=blue&link=https%3A%2F%2Ft.me%2Ftornadocli)](https://t.me/tornadocli) [![Element Badge](https://img.shields.io/badge/Join%20Element%20Chat-Element?style=flat&logo=element&color=green&link=https%3A%2F%2Felement.tornadocash.social%2F)](https://element.tornadocash.social) [![Discourse Badge](https://img.shields.io/badge/Discourse-Discourse?style=flat&logo=Discourse&color=black&link=https%3A%2F%2Fforum.tornado.ws%2F)](https://forum.tornado.ws/)
[![Telegram Badge](https://img.shields.io/badge/Join%20Group-telegram?style=flat&logo=telegram&color=blue&link=https%3A%2F%2Ft.me%2Ftornadoofficial)](https://t.me/tornadoofficial) [![Element Badge](https://img.shields.io/badge/Join%20Element%20Chat-Element?style=flat&logo=element&color=green&link=https%3A%2F%2Felement.tornadocash.social%2F)](https://element.tornadocash.social) [![Discourse Badge](https://img.shields.io/badge/Discourse-Discourse?style=flat&logo=Discourse&color=black&link=https%3A%2F%2Fforum.tornado.ws%2F)](https://forum.tornado.ws/)
</div>

304172
dist/cli.js vendored

File diff suppressed because one or more lines are too long

2649
dist/index.js vendored

File diff suppressed because it is too large Load Diff

2621
dist/index.mjs vendored

File diff suppressed because it is too large Load Diff

201496
dist/index.umd.js vendored Normal file

File diff suppressed because one or more lines are too long

9
dist/program.d.ts vendored
View File

@ -1,13 +1,14 @@
import 'dotenv/config';
import { Command } from 'commander';
import { JsonRpcProvider, Provider, TransactionLike, Wallet, VoidSigner, BigNumberish } from 'ethers';
import { getProviderOptions, TornadoWallet, TornadoVoidSigner, Relayer, RelayerInfo, RelayerError, RelayerClient, fetchDataOptions, Config } from './services';
import { JsonRpcProvider, Provider, TransactionLike, Wallet, VoidSigner } from 'ethers';
import { getProviderOptions, TornadoWallet, TornadoVoidSigner, Relayer, RelayerInfo, RelayerError, RelayerClient, fetchDataOptions, NetIdType, Config } from './services';
export type commonProgramOptions = {
rpc?: string;
ethRpc?: string;
graph?: string;
ethGraph?: string;
disableGraph?: boolean;
accountKey?: string;
relayer?: string;
walletWithdrawal?: boolean;
torPort?: number;
@ -29,7 +30,7 @@ export declare function getProgramOptions(options: commonProgramOptions): Promis
fetchDataOptions: fetchDataOptions;
}>;
export declare function getProgramGraphAPI(options: commonProgramOptions, config: Config): string;
export declare function getProgramProvider(netId: BigNumberish, rpcUrl: string | undefined, config: Config, providerOptions?: getProviderOptions): JsonRpcProvider;
export declare function getProgramProvider(netId: NetIdType, rpcUrl: string | undefined, config: Config, providerOptions?: getProviderOptions): JsonRpcProvider;
export declare function getProgramSigner({ options, provider, }: {
options: commonProgramOptions;
provider: Provider;
@ -37,7 +38,7 @@ export declare function getProgramSigner({ options, provider, }: {
export declare function getProgramRelayer({ options, fetchDataOptions, netId, }: {
options: commonProgramOptions;
fetchDataOptions?: fetchDataOptions;
netId: number | string;
netId: NetIdType;
}): Promise<{
validRelayers?: RelayerInfo[] | Relayer[];
invalidRelayers?: RelayerError[];

View File

@ -3,10 +3,10 @@ import { BaseEvents, MinimalEvents } from './events';
export declare function existsAsync(fileOrDir: string): Promise<boolean>;
export declare function zipAsync(file: AsyncZippable): Promise<Uint8Array>;
export declare function unzipAsync(data: Uint8Array): Promise<Unzipped>;
export declare function saveEvents<T extends MinimalEvents>({ name, userDirectory, events, }: {
name: string;
export declare function saveUserFile({ fileName, userDirectory, dataString, }: {
fileName: string;
userDirectory: string;
events: T[];
dataString: string;
}): Promise<void>;
export declare function loadSavedEvents<T extends MinimalEvents>({ name, userDirectory, deployedBlock, }: {
name: string;

View File

@ -1,7 +1,8 @@
import type { NetIdType } from './networkConfig';
export type DepositType = {
currency: string;
amount: string;
netId: string | number;
netId: NetIdType;
};
export type createDepositParams = {
nullifier: bigint;
@ -34,7 +35,7 @@ export declare function createDeposit({ nullifier, secret }: createDepositParams
export interface DepositConstructor {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
nullifier: bigint;
secret: bigint;
note: string;
@ -46,7 +47,7 @@ export interface DepositConstructor {
export declare class Deposit {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
nullifier: bigint;
secret: bigint;
note: string;
@ -65,7 +66,7 @@ export type parsedInvoiceExec = DepositType & {
export declare class Invoice {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
commitment: string;
invoice: string;
constructor(invoiceString: string);

48
dist/services/encryptedNotes.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
import { EthEncryptedData } from '@metamask/eth-sig-util';
import { Echoer } from '@tornado/contracts';
import { Wallet } from 'ethers';
import { EchoEvents, EncryptedNotesEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface NoteToEncrypt {
address: string;
noteHex: string;
}
export interface DecryptedNotes {
blockNumber: number;
address: string;
noteHex: string;
}
export declare function packEncryptedMessage({ nonce, ephemPublicKey, ciphertext }: EthEncryptedData): string;
export declare function unpackEncryptedMessage(encryptedMessage: string): EthEncryptedData & {
messageBuff: string;
};
export interface NoteAccountConstructor {
netId: NetIdType;
blockNumber?: number;
recoveryKey?: string;
Echoer: Echoer;
}
export declare class NoteAccount {
netId: NetIdType;
blockNumber?: number;
recoveryKey: string;
recoveryAddress: string;
recoveryPublicKey: string;
Echoer: Echoer;
constructor({ netId, blockNumber, recoveryKey, Echoer }: NoteAccountConstructor);
/**
* Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key
*/
static getWalletPublicKey(wallet: Wallet): string;
getEncryptedAccount(walletPublicKey: string): {
encryptedData: EthEncryptedData;
data: string;
};
/**
* Decrypt Echoer backuped note encryption account with private keys
*/
decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[];
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[];
encryptNote({ address, noteHex }: NoteToEncrypt): string;
}

View File

@ -1,12 +1,13 @@
import { BaseContract, Provider, EventLog, ContractEventName } from 'ethers';
import type { Tornado, TornadoRouter, TornadoProxyLight, Governance, RelayerRegistry } from '@tornado/contracts';
import type { Tornado, TornadoRouter, TornadoProxyLight, Governance, RelayerRegistry, Echoer } from '@tornado/contracts';
import { BatchEventsService, BatchBlockService, BatchTransactionService, BatchEventOnProgress, BatchBlockOnProgress } from '../batch';
import { fetchDataOptions } from '../providers';
import type { BaseEvents, MinimalEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, GovernanceProposalCreatedEvents, GovernanceVotedEvents, GovernanceDelegatedEvents, GovernanceUndelegatedEvents, RegistersEvents } from './types';
import type { NetIdType } from '../networkConfig';
import type { BaseEvents, MinimalEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, AllGovernanceEvents, RegistersEvents, EchoEvents } from './types';
export declare const DEPOSIT = "deposit";
export declare const WITHDRAWAL = "withdrawal";
export type BaseEventsServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -28,7 +29,7 @@ export type BaseGraphParams = {
onProgress?: BatchGraphOnProgress;
};
export declare class BaseEventsService<EventType extends MinimalEvents> {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -81,7 +82,7 @@ export declare class BaseEventsService<EventType extends MinimalEvents> {
}>;
}
export type BaseDepositsServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -110,8 +111,27 @@ export declare class BaseDepositsService extends BaseEventsService<DepositsEvent
events: (DepositsEvents | WithdrawalsEvents)[];
}): void;
}
export type BaseEchoServiceConstructor = {
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
Echoer: Echoer;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export declare class BaseEchoService extends BaseEventsService<EchoEvents> {
constructor({ netId, provider, graphApi, subgraphName, Echoer, deployedBlock, fetchDataOptions, }: BaseEchoServiceConstructor);
getInstanceName(): string;
getType(): string;
getGraphMethod(): string;
formatEvents(events: EventLog[]): Promise<EchoEvents[]>;
getEventsFromGraph({ fromBlock }: {
fromBlock: number;
}): Promise<BaseEvents<EchoEvents>>;
}
export type BaseEncryptedNotesServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -126,9 +146,8 @@ export declare class BaseEncryptedNotesService extends BaseEventsService<Encrypt
getGraphMethod(): string;
formatEvents(events: EventLog[]): Promise<EncryptedNotesEvents[]>;
}
export type BaseGovernanceEventTypes = GovernanceProposalCreatedEvents | GovernanceVotedEvents | GovernanceDelegatedEvents | GovernanceUndelegatedEvents;
export type BaseGovernanceServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -136,19 +155,19 @@ export type BaseGovernanceServiceConstructor = {
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export declare class BaseGovernanceService extends BaseEventsService<BaseGovernanceEventTypes> {
export declare class BaseGovernanceService extends BaseEventsService<AllGovernanceEvents> {
batchTransactionService: BatchTransactionService;
constructor({ netId, provider, graphApi, subgraphName, Governance, deployedBlock, fetchDataOptions, }: BaseGovernanceServiceConstructor);
getInstanceName(): string;
getType(): string;
getGraphMethod(): string;
formatEvents(events: EventLog[]): Promise<BaseGovernanceEventTypes[]>;
formatEvents(events: EventLog[]): Promise<AllGovernanceEvents[]>;
getEventsFromGraph({ fromBlock }: {
fromBlock: number;
}): Promise<BaseEvents<BaseGovernanceEventTypes>>;
}): Promise<BaseEvents<AllGovernanceEvents>>;
}
export type BaseRegistryServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;

View File

@ -1,6 +1,6 @@
import { BatchBlockOnProgress, BatchEventOnProgress } from '../batch';
import { BaseDepositsService, BaseEncryptedNotesService, BaseGovernanceService, BaseRegistryService, BaseDepositsServiceConstructor, BaseEncryptedNotesServiceConstructor, BaseGovernanceServiceConstructor, BaseRegistryServiceConstructor, BaseGovernanceEventTypes } from './base';
import type { BaseEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, RegistersEvents } from './types';
import { BaseDepositsService, BaseEncryptedNotesService, BaseGovernanceService, BaseRegistryService, BaseDepositsServiceConstructor, BaseEncryptedNotesServiceConstructor, BaseGovernanceServiceConstructor, BaseRegistryServiceConstructor, BaseEchoServiceConstructor, BaseEchoService } from './base';
import type { BaseEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, RegistersEvents, AllGovernanceEvents, EchoEvents } from './types';
export type NodeDepositsServiceConstructor = BaseDepositsServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
@ -17,6 +17,20 @@ export declare class NodeDepositsService extends BaseDepositsService {
getEventsFromCache(): Promise<BaseEvents<DepositsEvents | WithdrawalsEvents>>;
saveEvents({ events, lastBlock }: BaseEvents<DepositsEvents | WithdrawalsEvents>): Promise<void>;
}
export type NodeEchoServiceConstructor = BaseEchoServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
};
export declare class NodeEchoService extends BaseEchoService {
cacheDirectory?: string;
userDirectory?: string;
constructor({ netId, provider, graphApi, subgraphName, Echoer, deployedBlock, fetchDataOptions, cacheDirectory, userDirectory, }: NodeEchoServiceConstructor);
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]): void;
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]): void;
getEventsFromDB(): Promise<BaseEvents<EchoEvents>>;
getEventsFromCache(): Promise<BaseEvents<EchoEvents>>;
saveEvents({ events, lastBlock }: BaseEvents<EchoEvents>): Promise<void>;
}
export type NodeEncryptedNotesServiceConstructor = BaseEncryptedNotesServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
@ -42,9 +56,9 @@ export declare class NodeGovernanceService extends BaseGovernanceService {
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]): void;
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]): void;
updateTransactionProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]): void;
getEventsFromDB(): Promise<BaseEvents<BaseGovernanceEventTypes>>;
getEventsFromCache(): Promise<BaseEvents<BaseGovernanceEventTypes>>;
saveEvents({ events, lastBlock }: BaseEvents<BaseGovernanceEventTypes>): Promise<void>;
getEventsFromDB(): Promise<BaseEvents<AllGovernanceEvents>>;
getEventsFromCache(): Promise<BaseEvents<AllGovernanceEvents>>;
saveEvents({ events, lastBlock }: BaseEvents<AllGovernanceEvents>): Promise<void>;
}
export type NodeRegistryServiceConstructor = BaseRegistryServiceConstructor & {
cacheDirectory?: string;

View File

@ -39,6 +39,7 @@ export type GovernanceUndelegatedEvents = GovernanceEvents & {
account: string;
delegateFrom: string;
};
export type AllGovernanceEvents = GovernanceProposalCreatedEvents | GovernanceVotedEvents | GovernanceDelegatedEvents | GovernanceUndelegatedEvents;
export type RegistersEvents = MinimalEvents & RelayerParams;
export type DepositsEvents = MinimalEvents & {
commitment: string;
@ -52,6 +53,10 @@ export type WithdrawalsEvents = MinimalEvents & {
fee: string;
timestamp: number;
};
export type EchoEvents = MinimalEvents & {
address: string;
encryptedAccount: string;
};
export type EncryptedNotesEvents = MinimalEvents & {
encryptedNote: string;
};

View File

@ -1,5 +1,5 @@
import { fetchDataOptions } from '../providers';
import type { BaseGraphEvents, RegistersEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, BatchGraphOnProgress } from '../events';
import type { BaseGraphEvents, RegistersEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, BatchGraphOnProgress, EchoEvents, AllGovernanceEvents } from '../events';
export * from './queries';
export type queryGraphParams = {
graphApi: string;
@ -165,6 +165,29 @@ export interface getNoteAccountsReturns {
lastSyncBlock: null | number;
}
export declare function getNoteAccounts({ graphApi, subgraphName, address, fetchDataOptions, }: getNoteAccountsParams): Promise<getNoteAccountsReturns>;
export interface GraphEchoEvents {
noteAccounts: {
id: string;
blockNumber: string;
address: string;
encryptedAccount: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getGraphEchoEventsParams {
graphApi: string;
subgraphName: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export declare function getGraphEchoEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions, }: getGraphEchoEventsParams): Promise<GraphEchoEvents>;
export declare function getAllGraphEchoEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions, onProgress, }: getGraphEchoEventsParams): Promise<BaseGraphEvents<EchoEvents>>;
export interface GraphEncryptedNotes {
encryptedNotes: {
blockNumber: string;
@ -188,3 +211,56 @@ export interface getEncryptedNotesParams {
}
export declare function getEncryptedNotes({ graphApi, subgraphName, fromBlock, fetchDataOptions, }: getEncryptedNotesParams): Promise<GraphEncryptedNotes>;
export declare function getAllEncryptedNotes({ graphApi, subgraphName, fromBlock, fetchDataOptions, onProgress, }: getEncryptedNotesParams): Promise<BaseGraphEvents<EncryptedNotesEvents>>;
export interface GraphGovernanceEvents {
proposals: {
blockNumber: number;
logIndex: number;
transactionHash: string;
proposalId: number;
proposer: string;
target: string;
startTime: number;
endTime: number;
description: string;
}[];
votes: {
blockNumber: number;
logIndex: number;
transactionHash: string;
proposalId: number;
voter: string;
support: boolean;
votes: string;
from: string;
input: string;
}[];
delegates: {
blockNumber: number;
logIndex: number;
transactionHash: string;
account: string;
delegateTo: string;
}[];
undelegates: {
blockNumber: number;
logIndex: number;
transactionHash: string;
account: string;
delegateFrom: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getGovernanceEventsParams {
graphApi: string;
subgraphName: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export declare function getGovernanceEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions, }: getGovernanceEventsParams): Promise<GraphGovernanceEvents>;
export declare function getAllGovernanceEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions, onProgress, }: getGovernanceEventsParams): Promise<BaseGraphEvents<AllGovernanceEvents>>;

View File

@ -4,4 +4,7 @@ export declare const GET_REGISTERED = "\n query getRegistered($first: Int, $fro
export declare const GET_DEPOSITS = "\n query getDeposits($currency: String!, $amount: String!, $first: Int, $fromBlock: Int) {\n deposits(first: $first, orderBy: index, orderDirection: asc, where: { \n amount: $amount,\n currency: $currency,\n blockNumber_gte: $fromBlock\n }) {\n id\n blockNumber\n commitment\n index\n timestamp\n from\n }\n _meta {\n block {\n number\n }\n hasIndexingErrors\n }\n }\n";
export declare const GET_WITHDRAWALS = "\n query getWithdrawals($currency: String!, $amount: String!, $first: Int, $fromBlock: Int!) {\n withdrawals(first: $first, orderBy: blockNumber, orderDirection: asc, where: { \n currency: $currency,\n amount: $amount,\n blockNumber_gte: $fromBlock\n }) {\n id\n blockNumber\n nullifier\n to\n fee\n timestamp\n }\n _meta {\n block {\n number\n }\n hasIndexingErrors\n }\n }\n";
export declare const GET_NOTE_ACCOUNTS = "\n query getNoteAccount($address: String!) {\n noteAccounts(where: { address: $address }) {\n id\n index\n address\n encryptedAccount\n }\n _meta {\n block {\n number\n }\n hasIndexingErrors\n }\n }\n";
export declare const GET_ECHO_EVENTS = "\n query getNoteAccounts($first: Int, $fromBlock: Int) {\n noteAccounts(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {\n id\n blockNumber\n address\n encryptedAccount\n }\n _meta {\n block {\n number\n }\n hasIndexingErrors\n }\n }\n";
export declare const GET_ENCRYPTED_NOTES = "\n query getEncryptedNotes($first: Int, $fromBlock: Int) {\n encryptedNotes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {\n blockNumber\n index\n transactionHash\n encryptedNote\n }\n _meta {\n block {\n number\n }\n hasIndexingErrors\n }\n }\n";
export declare const GET_GOVERNANCE_EVENTS = "\n query getGovernanceEvents($first: Int, $fromBlock: Int) {\n proposals(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {\n blockNumber\n logIndex\n transactionHash\n proposalId\n proposer\n target\n startTime\n endTime\n description\n }\n votes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {\n blockNumber\n logIndex\n transactionHash\n proposalId\n voter\n support\n votes\n from\n input\n }\n delegates(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {\n blockNumber\n logIndex\n transactionHash\n account\n delegateTo\n }\n undelegates(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {\n blockNumber\n logIndex\n transactionHash\n account\n delegateFrom\n }\n _meta {\n block {\n number\n }\n hasIndexingErrors\n }\n }\n";
export declare const GET_GOVERNANCE_APY = "\n stakeDailyBurns(first: 30, orderBy: date, orderDirection: desc) {\n id\n date\n dailyAmountBurned\n }\n";

View File

@ -4,6 +4,7 @@ export * from './schemas';
export * from './batch';
export * from './data';
export * from './deposits';
export * from './encryptedNotes';
export * from './fees';
export * from './merkleTree';
export * from './mimc';
@ -15,5 +16,6 @@ export * from './prices';
export * from './providers';
export * from './relayerClient';
export * from './tokens';
export * from './treeCache';
export * from './utils';
export * from './websnark';

View File

@ -1,10 +1,11 @@
import { MerkleTree, Element } from '@tornado/fixed-merkle-tree';
import { MerkleTree, PartialMerkleTree, Element, TreeEdge } from '@tornado/fixed-merkle-tree';
import type { Tornado } from '@tornado/contracts';
import type { DepositType } from './deposits';
import type { DepositsEvents } from './events';
import type { NetIdType } from './networkConfig';
export type MerkleTreeConstructor = DepositType & {
Tornado: Tornado;
commitment?: string;
commitmentHex?: string;
merkleTreeHeight?: number;
emptyElement?: string;
merkleWorkerPath?: string;
@ -12,18 +13,18 @@ export type MerkleTreeConstructor = DepositType & {
export declare class MerkleTreeService {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
Tornado: Tornado;
commitment?: string;
commitmentHex?: string;
instanceName: string;
merkleTreeHeight: number;
emptyElement: string;
merkleWorkerPath?: string;
constructor({ netId, amount, currency, Tornado, commitment, merkleTreeHeight, emptyElement, merkleWorkerPath, }: MerkleTreeConstructor);
createTree({ events }: {
events: Element[];
}): Promise<MerkleTree>;
verifyTree({ events }: {
events: DepositsEvents[];
}): Promise<MerkleTree>;
constructor({ netId, amount, currency, Tornado, commitmentHex, merkleTreeHeight, emptyElement, merkleWorkerPath, }: MerkleTreeConstructor);
createTree(events: Element[]): Promise<MerkleTree>;
createPartialTree({ edge, elements }: {
edge: TreeEdge;
elements: Element[];
}): Promise<PartialMerkleTree>;
verifyTree(events: DepositsEvents[]): Promise<MerkleTree>;
}

View File

@ -1,3 +1,17 @@
/**
* Type of default supported networks
*/
export declare enum NetId {
MAINNET = 1,
BSC = 56,
POLYGON = 137,
OPTIMISM = 10,
ARBITRUM = 42161,
GNOSIS = 100,
AVALANCHE = 43114,
SEPOLIA = 11155111
}
export type NetIdType = NetId | number;
export interface RpcUrl {
name: string;
url: string;
@ -37,20 +51,20 @@ export type Config = {
};
nativeCurrency: string;
currencyName: string;
explorerUrl: {
tx: string;
address: string;
block: string;
};
explorerUrl: string;
merkleTreeHeight: number;
emptyElement: string;
networkName: string;
deployedBlock: number;
rpcUrls: RpcUrls;
multicall: string;
multicallContract: string;
routerContract: string;
registryContract?: string;
echoContract: string;
offchainOracleContract?: string;
tornContract?: string;
governanceContract?: string;
stakingRewardsContract?: string;
registryContract?: string;
aggregatorContract?: string;
reverseRecordsContract?: string;
gasPriceOracleContract?: string;
@ -58,6 +72,7 @@ export type Config = {
ovmGasPriceOracleContract?: string;
tornadoSubgraph: string;
registrySubgraph?: string;
governanceSubgraph?: string;
subgraphs: SubgraphUrls;
tokens: TokenInstances;
optionalTokens?: string[];
@ -70,17 +85,32 @@ export type Config = {
REGISTRY_BLOCK?: number;
MINING_BLOCK_TIME?: number;
};
'torn.contract.tornadocash.eth'?: string;
'governance.contract.tornadocash.eth'?: string;
'staking-rewards.contract.tornadocash.eth'?: string;
'tornado-router.contract.tornadocash.eth'?: string;
'tornado-proxy-light.contract.tornadocash.eth'?: string;
};
export type networkConfig = {
[key in string]: Config;
[key in NetIdType]: Config;
};
export declare const blockSyncInterval = 10000;
export declare const enabledChains: string[];
export declare const networkConfig: networkConfig;
export declare const subdomains: string[];
export default networkConfig;
export declare const defaultConfig: networkConfig;
export declare const enabledChains: number[];
/**
* Custom config object to extend default config
*
* Inspired by getUrlFunc from ethers.js
* https://github.com/ethers-io/ethers.js/blob/v6/src.ts/utils/fetch.ts#L59
*/
export declare let customConfig: networkConfig;
/**
* Add or override existing network config object
*
* Could be also called on the UI hook so that the UI could allow people to use custom privacy pools
*/
export declare function addNetwork(newConfig: networkConfig): void;
export declare function getNetworkConfig(): networkConfig;
export declare function getConfig(netId: NetIdType): Config;
export declare function getInstanceByAddress({ netId, address }: {
netId: NetIdType;
address: string;
}): {
amount: string;
currency: string;
} | undefined;
export declare function getSubdomains(): string[];

View File

@ -4,3 +4,7 @@ export declare function parseRelayer(value?: string): string;
export declare function parseAddress(value?: string): string;
export declare function parseMnemonic(value?: string): string;
export declare function parseKey(value?: string): string;
/**
* Recovery key shouldn't have a 0x prefix (Also this is how the UI generates)
*/
export declare function parseRecoveryKey(value?: string): string;

View File

@ -3,9 +3,9 @@
/// <reference types="node" />
import type { EventEmitter } from 'stream';
import type { RequestOptions } from 'http';
import { JsonRpcApiProvider, JsonRpcProvider, Wallet, FetchGetUrlFunc, Provider, SigningKey, TransactionRequest, JsonRpcSigner, BrowserProvider, Networkish, Eip1193Provider, VoidSigner, FetchUrlFeeDataNetworkPlugin, BigNumberish } from 'ethers';
import { JsonRpcApiProvider, JsonRpcProvider, Wallet, FetchGetUrlFunc, Provider, SigningKey, TransactionRequest, JsonRpcSigner, BrowserProvider, Networkish, Eip1193Provider, VoidSigner, FetchUrlFeeDataNetworkPlugin } from 'ethers';
import type { RequestInfo, RequestInit, Response, HeadersInit } from 'node-fetch';
import type { Config } from './networkConfig';
import type { Config, NetIdType } from './networkConfig';
declare global {
interface Window {
ethereum?: Eip1193Provider & EventEmitter;
@ -41,7 +41,7 @@ export type getProviderOptions = fetchDataOptions & {
};
export declare function getGasOraclePlugin(networkKey: string, fetchOptions?: getProviderOptions): FetchUrlFeeDataNetworkPlugin;
export declare function getProvider(rpcUrl: string, fetchOptions?: getProviderOptions): Promise<JsonRpcProvider>;
export declare function getProviderWithNetId(netId: BigNumberish, rpcUrl: string, config: Config, fetchOptions?: getProviderOptions): JsonRpcProvider;
export declare function getProviderWithNetId(netId: NetIdType, rpcUrl: string, config: Config, fetchOptions?: getProviderOptions): JsonRpcProvider;
export declare const populateTransaction: (signer: TornadoWallet | TornadoVoidSigner | TornadoRpcSigner, tx: TransactionRequest) => Promise<TransactionRequest>;
export type TornadoWalletOptions = {
gasPriceBump?: number;
@ -80,7 +80,7 @@ export declare class TornadoRpcSigner extends JsonRpcSigner {
export type connectWalletFunc = (...args: any[]) => Promise<void>;
export type handleWalletFunc = (...args: any[]) => void;
export type TornadoBrowserProviderOptions = TornadoWalletOptions & {
webChainId?: BigNumberish;
webChainId?: NetIdType;
connectWallet?: connectWalletFunc;
handleNetworkChanges?: handleWalletFunc;
handleAccountChanges?: handleWalletFunc;

View File

@ -1,6 +1,6 @@
import type { Aggregator } from '@tornado/contracts';
import type { RelayerStructOutput } from '@tornado/contracts/dist/contracts/Governance/Aggregator/Aggregator';
import type { Config } from './networkConfig';
import { NetIdType, Config } from './networkConfig';
import { fetchDataOptions } from './providers';
import type { snarkProofs } from './websnark';
export declare const MIN_STAKE_BALANCE: bigint;
@ -9,20 +9,22 @@ export interface RelayerParams {
relayerAddress?: string;
}
export interface Relayer {
netId: number;
netId: NetIdType;
url: string;
hostname: string;
rewardAccount: string;
instances: string[];
gasPrice?: number;
ethPrices?: {
[key in string]: string;
};
currentQueue: number;
tornadoServiceFee: number;
}
export type RelayerInfo = Relayer & {
hostname: string;
ensName: string;
stakeBalance: bigint;
relayerAddress: string;
ethPrices?: {
[key in string]: string;
};
};
export type RelayerError = {
hostname: string;
@ -46,7 +48,7 @@ export interface RelayerStatus {
fast: number;
additionalProperties?: number;
};
netId: number;
netId: NetIdType;
ethPrices?: {
[key in string]: string;
};
@ -84,12 +86,20 @@ export interface semanticVersion {
buildmetadata?: string;
}
export declare function parseSemanticVersion(version: string): semanticVersion;
export declare function isRelayerUpdated(relayerVersion: string, netId: number | string): boolean;
export declare function isRelayerUpdated(relayerVersion: string, netId: NetIdType): boolean;
export declare function calculateScore({ stakeBalance, tornadoServiceFee }: RelayerInfo, minFee?: number, maxFee?: number): bigint;
export declare function getWeightRandom(weightsScores: bigint[], random: bigint): number;
export declare function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: string | number): RelayerInfo;
export type RelayerInstanceList = {
[key in string]: {
instanceAddress: {
[key in string]: string;
};
};
};
export declare function getSupportedInstances(instanceList: RelayerInstanceList): string[];
export declare function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: NetIdType): RelayerInfo;
export interface RelayerClientConstructor {
netId: number | string;
netId: NetIdType;
config: Config;
Aggregator: Aggregator;
fetchDataOptions?: fetchDataOptions;
@ -98,7 +108,7 @@ export type RelayerClientWithdraw = snarkProofs & {
contract: string;
};
export declare class RelayerClient {
netId: number;
netId: NetIdType;
config: Config;
Aggregator: Aggregator;
selectedRelayer?: Relayer;

View File

@ -1,4 +1,4 @@
import type { Config } from '../networkConfig';
import { Config, NetIdType } from '../networkConfig';
export type statusInstanceType = {
type: string;
properties: {
@ -88,5 +88,5 @@ declare const bnType: {
type: string;
BN: boolean;
};
export declare function getStatusSchema(netId: number | string, config: Config): statusSchema;
export declare function getStatusSchema(netId: NetIdType, config: Config): statusSchema;
export {};

35
dist/services/treeCache.d.ts vendored Normal file
View File

@ -0,0 +1,35 @@
/**
* Create tree cache file from node.js
*
* Only works for node.js, modified from https://github.com/tornadocash/tornado-classic-ui/blob/master/scripts/updateTree.js
*/
import { MerkleTree } from '@tornado/fixed-merkle-tree';
import { DepositsEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface TreeCacheConstructor {
netId: NetIdType;
amount: string;
currency: string;
userDirectory: string;
PARTS_COUNT?: number;
LEAVES?: number;
zeroElement?: string;
}
export interface treeMetadata {
blockNumber: number;
logIndex: number;
transactionHash: string;
timestamp: number;
from: string;
leafIndex: number;
}
export declare class TreeCache {
netId: NetIdType;
amount: string;
currency: string;
userDirectory: string;
PARTS_COUNT: number;
constructor({ netId, amount, currency, userDirectory, PARTS_COUNT }: TreeCacheConstructor);
getInstanceName(): string;
createTree(events: DepositsEvents[], tree: MerkleTree): Promise<void>;
}

View File

@ -1,15 +1,20 @@
/// <reference types="node" />
/// <reference types="node" />
import { webcrypto } from 'crypto';
import BN from 'bn.js';
import type { BigNumberish } from 'ethers';
type bnInput = number | string | number[] | Uint8Array | Buffer | BN;
export declare const isNode: boolean;
export declare const crypto: webcrypto.Crypto;
export declare const chunk: <T>(arr: T[], size: number) => T[][];
export declare function sleep(ms: number): Promise<unknown>;
export declare function validateUrl(url: string, protocols?: string[]): boolean;
export declare function concatBytes(...arrays: Uint8Array[]): Uint8Array;
export declare function bufferToBytes(b: Buffer): Uint8Array;
export declare function bytesToBase64(bytes: Uint8Array): string;
export declare function base64ToBytes(base64: string): Uint8Array;
export declare function bytesToHex(bytes: Uint8Array): string;
export declare function hexToBytes(hexString: string): Uint8Array;
export declare function bytesToBN(bytes: Uint8Array): bigint;
export declare function bnToBytes(bigint: bigint | string): Uint8Array;
export declare function leBuff2Int(bytes: Uint8Array): BN;

View File

@ -1,10 +1,12 @@
{
"name": "tornado-cli",
"version": "1.0.1-alpha",
"version": "1.0.3-alpha",
"description": "Modern Toolsets for Privacy Pools on Ethereum",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"unpkg": "./dist/index.umd.js",
"jsdelivr": "./dist/index.umd.js",
"bin": {
"tornado-cli": "./dist/cli.js"
},
@ -12,7 +14,9 @@
"typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json",
"types": "tsc --declaration --emitDeclarationOnly",
"lint": "eslint src/**/*.ts --ext .ts --ignore-pattern src/typechain",
"build": "yarn types && rollup -c",
"build:node": "ts-node scripts/fflate.ts && rollup -c",
"build:web": "webpack",
"build": "yarn types && yarn build:node && yarn build:web",
"start": "ts-node src/cli.ts",
"startHelp": "ts-node src/cli.ts help",
"createDeposit": "ts-node src/cli.ts create",
@ -20,8 +24,10 @@
"depositInvoice": "ts-node src/cli.ts depositInvoice",
"withdraw": "ts-node src/cli.ts withdraw",
"compliance": "ts-node src/cli.ts compliance",
"syncEvents": "ts-node src/cli.ts syncEvents",
"updateEvents": "ts-node src/cli.ts updateEvents",
"relayers": "ts-node src/cli.ts relayers",
"createAccount": "ts-node src/cli.ts createAccount",
"decryptNotes": "ts-node src/cli.ts decryptNotes",
"send": "ts-node src/cli.ts send",
"balance": "ts-node src/cli.ts balance",
"sign": "ts-node src/cli.ts sign",
@ -46,23 +52,24 @@
"yarn.lock"
],
"dependencies": {
"@colors/colors": "1.5.0",
"@metamask/eth-sig-util": "^7.0.1",
"@tornado/contracts": "1.0.0",
"@tornado/fixed-merkle-tree": "0.7.3",
"@tornado/snarkjs": "0.1.20",
"@tornado/websnark": "0.0.4",
"ajv": "^8.12.0",
"bignumber.js": "^9.1.2",
"bloomfilter.js": "^1.0.2",
"bn.js": "^5.2.1",
"circomlibjs": "0.1.7",
"cli-table3": "^0.6.4",
"commander": "^12.0.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"ethers": "^6.12.0",
"ffjavascript": "0.2.48",
"fflate": "^0.8.2",
"figlet": "^1.7.0",
"fflate": "^0.8.2"
},
"optionalDependencies": {
"@colors/colors": "1.5.0",
"cli-table3": "^0.6.4",
"commander": "^12.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.4",
"moment": "^2.30.1",
@ -72,7 +79,6 @@
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@typechain/ethers-v6": "^0.5.1",
"@types/bn.js": "^5.1.5",
"@types/circomlibjs": "^0.1.6",
@ -81,18 +87,24 @@
"@types/node-fetch": "^2.6.11",
"@typescript-eslint/eslint-plugin": "^7.6.0",
"@typescript-eslint/parser": "^7.6.0",
"dotenv": "^16.4.5",
"esbuild": "^0.20.2",
"esbuild-loader": "^4.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"figlet": "^1.7.0",
"node-polyfill-webpack-plugin": "^3.0.0",
"prettier": "^3.2.5",
"rollup": "^4.14.1",
"rollup-plugin-esbuild": "^6.1.1",
"ts-node": "^10.9.2",
"tsc": "^2.0.4",
"typechain": "^8.3.2",
"typescript": "^5.4.4"
"typescript": "^5.4.4",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
}
}

View File

@ -2,13 +2,15 @@ import esbuild from 'rollup-plugin-esbuild';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import pkgJson from './package.json' assert { type: 'json' };
const external = Object.keys(pkgJson.dependencies).concat(...[
'@tornado/websnark/src/utils',
'@tornado/websnark/src/groth16',
]);
const external = Object.keys(pkgJson.dependencies).concat(
Object.keys(pkgJson.optionalDependencies),
[
'@tornado/websnark/src/utils',
'@tornado/websnark/src/groth16',
]
);
const config = [
{
@ -28,8 +30,8 @@ const config = [
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
nodeResolve(),
json()
],
},
@ -53,28 +55,6 @@ const config = [
json()
],
},
{
input: 'src/cli.ts',
output: [
{
file: pkgJson.bin[pkgJson.name],
format: "cjs",
esModule: false,
banner: '#!/usr/bin/env node\n'
},
],
plugins: [
esbuild({
include: /\.[jt]sx?$/,
minify: false,
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
json()
],
},
{
input: 'src/merkleTreeWorker.ts',
output: [
@ -92,36 +72,10 @@ const config = [
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
nodeResolve(),
json()
],
},
{
input: 'src/merkleTreeWorker.ts',
output: [
{
file: 'static/merkleTreeWorker.umd.js',
format: "umd",
esModule: false
},
],
treeshake: 'smallest',
external: ['web-worker'],
plugins: [
esbuild({
include: /\.[jt]sx?$/,
minify: false,
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
json(),
replace({
'process.browser': 'true'
})
],
}
]

39
scripts/fflate.ts Normal file
View File

@ -0,0 +1,39 @@
/**
* Correct the resolve field of fflate as we don't use browser esm
*
* See issue https://github.com/101arrowz/fflate/issues/211
*/
import fs from 'fs';
const pkgJson = JSON.parse(fs.readFileSync('./node_modules/fflate/package.json', { encoding: 'utf8' }));
const backupJson = JSON.stringify(pkgJson, null, 2);
let changes = false
if (pkgJson.module.includes('browser')) {
pkgJson.module = './esm/index.mjs';
changes = true;
}
if (pkgJson.exports['.']?.import?.types && pkgJson.exports?.['.']?.import?.types.includes('browser')) {
pkgJson.exports['.'] = {
...pkgJson.exports['.'],
"import": {
"types": "./esm/index.d.mts",
"default": "./esm/index.mjs"
},
"require": {
"types": "./lib/index.d.ts",
"default": "./lib/index.cjs"
}
}
changes = true;
}
if (changes) {
fs.writeFileSync('./node_modules/fflate/package.backup.json', backupJson + '\n');
fs.writeFileSync('./node_modules/fflate/package.json', JSON.stringify(pkgJson, null, 2) + '\n');
}

View File

@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any, prettier/prettier */
/* eslint-disable @typescript-eslint/no-explicit-any */
import workerThreads from 'worker_threads';
import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree';
import { mimc, isNode } from './services';
@ -65,5 +65,5 @@ if (isNode && workerThreads) {
postMessage(merkleTree.toString());
});
} else {
throw new Error('This browser / environment doesn\'t support workers!');
throw new Error('This browser / environment does not support workers!');
}

View File

@ -13,6 +13,7 @@ import {
RelayerRegistry__factory,
Aggregator__factory,
Governance__factory,
Echoer__factory,
} from '@tornado/contracts';
import {
JsonRpcProvider,
@ -27,7 +28,7 @@ import {
ZeroAddress,
MaxUint256,
Transaction,
BigNumberish,
getAddress,
} from 'ethers';
import type MerkleTree from '@tornado/fixed-merkle-tree';
import * as packageJson from '../package.json';
@ -62,6 +63,7 @@ import {
TornadoFeeOracle,
TokenPriceOracle,
calculateSnarkProof,
NodeEchoService,
NodeEncryptedNotesService,
NodeGovernanceService,
RelayerClient,
@ -70,27 +72,34 @@ import {
Invoice,
fetchData,
fetchDataOptions,
networkConfig,
subdomains,
NetId,
NetIdType,
getInstanceByAddress,
getSubdomains,
getConfig,
Config,
enabledChains,
substring,
NoteAccount,
parseRecoveryKey,
getSupportedInstances,
TreeCache,
} from './services';
const DEFAULT_GAS_LIMIT = 600_000;
const RELAYER_NETWORK = 1;
const TOKEN_PRICE_ORACLE = '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8';
const DEFAULT_GAS_LIMIT = Number(process.env.DEFAULT_GAS_LIMIT) || 600_000;
const RELAYER_NETWORK = Number(process.env.RELAYER_NETWORK) || NetId.MAINNET;
// Where cached events, trees, circuits, and key is saved
const STATIC_DIR = process.env.CACHE_DIR || path.join(__dirname, '../static');
const EVENTS_DIR = path.join(STATIC_DIR, './events');
const TREES_DIR = path.join(STATIC_DIR, './trees');
const MERKLE_WORKER_PATH =
process.env.DISABLE_MERKLE_WORKER === 'true' ? undefined : path.join(STATIC_DIR, './merkleTreeWorker.js');
// Where we should backup notes and save events
const USER_DIR = process.env.USER_DIR || '.';
const SAVED_DIR = path.join(USER_DIR, './events');
const SAVED_TREE_DIR = path.join(USER_DIR, './trees');
const CIRCUIT_PATH = path.join(__dirname, '../static/tornado.json');
const KEY_PATH = path.join(__dirname, '../static/tornadoProvingKey.bin');
@ -107,6 +116,7 @@ export type commonProgramOptions = {
graph?: string;
ethGraph?: string;
disableGraph?: boolean;
accountKey?: string;
relayer?: string;
walletWithdrawal?: boolean;
torPort?: number;
@ -161,6 +171,7 @@ export async function getProgramOptions(options: commonProgramOptions): Promise<
graph: options.graph || (process.env.GRAPH_URL ? parseUrl(process.env.GRAPH_URL) : undefined),
ethGraph: options.ethGraph || (process.env.ETHGRAPH_URL ? parseUrl(process.env.ETHGRAPH_URL) : undefined),
disableGraph: Boolean(options.disableGraph) || (process.env.DISABLE_GRAPH === 'true' ? true : undefined),
accountKey: options.accountKey || (process.env.ACCOUNT_KEY ? parseRecoveryKey(process.env.ACCOUNT_KEY) : undefined),
relayer: options.relayer || (process.env.RELAYER ? parseRelayer(process.env.RELAYER) : undefined),
walletWithdrawal:
Boolean(options.walletWithdrawal) || (process.env.WALLET_WITHDRAWAL === 'true' ? true : undefined),
@ -221,7 +232,7 @@ export function getProgramGraphAPI(options: commonProgramOptions, config: Config
}
export function getProgramProvider(
netId: BigNumberish,
netId: NetIdType,
rpcUrl: string = '',
config: Config,
providerOptions?: getProviderOptions,
@ -260,7 +271,7 @@ export async function getProgramRelayer({
}: {
options: commonProgramOptions;
fetchDataOptions?: fetchDataOptions;
netId: number | string;
netId: NetIdType;
}): Promise<{
validRelayers?: RelayerInfo[] | Relayer[];
invalidRelayers?: RelayerError[];
@ -268,9 +279,11 @@ export async function getProgramRelayer({
}> {
const { ethRpc, ethGraph, relayer, disableGraph } = options;
const netConfig = networkConfig[`netId${netId}`];
const netConfig = getConfig(netId);
const ethConfig = networkConfig[`netId${RELAYER_NETWORK}`];
const ethConfig = getConfig(RELAYER_NETWORK);
const subdomains = getSubdomains();
const {
aggregatorContract,
@ -279,7 +292,7 @@ export async function getProgramRelayer({
constants: { REGISTRY_BLOCK },
} = ethConfig;
const provider = getProgramProvider(1, ethRpc, ethConfig, {
const provider = getProgramProvider(RELAYER_NETWORK, ethRpc, ethConfig, {
...fetchDataOptions,
});
@ -320,7 +333,11 @@ export async function getProgramRelayer({
relayerClient.selectedRelayer = {
netId: relayerStatus.netId,
url: relayerStatus.url,
rewardAccount: relayerStatus.rewardAccount,
hostname: new URL(relayerStatus.url).hostname,
rewardAccount: getAddress(relayerStatus.rewardAccount),
instances: getSupportedInstances(relayerStatus.instances),
gasPrice: relayerStatus.gasPrices?.fast,
ethPrices: relayerStatus.ethPrices,
currentQueue: relayerStatus.currentQueue,
tornadoServiceFee: relayerStatus.tornadoServiceFee,
};
@ -336,13 +353,7 @@ export async function getProgramRelayer({
const relayerStatus = validRelayers[0];
if (relayerStatus) {
relayerClient.selectedRelayer = {
netId: relayerStatus.netId,
url: relayerStatus.url,
rewardAccount: relayerStatus.rewardAccount,
currentQueue: relayerStatus.currentQueue,
tornadoServiceFee: relayerStatus.tornadoServiceFee,
};
relayerClient.selectedRelayer = relayerStatus;
}
return {
@ -363,13 +374,7 @@ export async function getProgramRelayer({
const relayerStatus = relayerClient.pickWeightedRandomRelayer(validRelayers);
if (relayerStatus) {
relayerClient.selectedRelayer = {
netId: relayerStatus.netId,
url: relayerStatus.url,
rewardAccount: relayerStatus.rewardAccount,
currentQueue: relayerStatus.currentQueue,
tornadoServiceFee: relayerStatus.tornadoServiceFee,
};
relayerClient.selectedRelayer = relayerStatus;
}
return {
@ -454,10 +459,10 @@ export function tornadoProgram() {
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.argument('<currency>', 'Currency to deposit on Tornado Cash')
.argument('<amount>', 'Amount to deposit on Tornado Cash')
.action(async (netId: string | number, currency: string, amount: string) => {
.action(async (netId: NetIdType, currency: string, amount: string) => {
currency = currency.toLowerCase();
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
routerContract,
@ -514,16 +519,17 @@ export function tornadoProgram() {
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.argument('<currency>', 'Currency to deposit on Tornado Cash')
.argument('<amount>', 'Amount to deposit on Tornado Cash')
.action(async (netId: string | number, currency: string, amount: string, cmdOptions: commonProgramOptions) => {
.action(async (netId: NetIdType, currency: string, amount: string, cmdOptions: commonProgramOptions) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
currency = currency.toLowerCase();
const { rpc } = options;
const { rpc, accountKey } = options;
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
multicall: multicallAddress,
multicallContract,
routerContract,
echoContract,
nativeCurrency,
tokens: { [currency]: currencyConfig },
} = config;
@ -546,6 +552,14 @@ export function tornadoProgram() {
provider,
});
const noteAccount = accountKey
? new NoteAccount({
netId,
recoveryKey: accountKey,
Echoer: Echoer__factory.connect(echoContract, provider),
})
: undefined;
if (!signer) {
throw new Error(
'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured',
@ -553,7 +567,7 @@ export function tornadoProgram() {
}
const TornadoProxy = TornadoRouter__factory.connect(routerContract, signer);
const Multicall = Multicall__factory.connect(multicallAddress, provider);
const Multicall = Multicall__factory.connect(multicallContract, provider);
const Token = tokenAddress ? ERC20__factory.connect(tokenAddress, signer) : undefined;
const [ethBalance, tokenBalance, tokenApprovals] = await multicall(Multicall, [
@ -562,22 +576,20 @@ export function tornadoProgram() {
name: 'getEthBalance',
params: [signer.address],
},
/* eslint-disable prettier/prettier */
...(!isEth
? [
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
: []),
/* eslint-enable prettier/prettier */
]);
if (isEth && denomination > ethBalance) {
@ -609,18 +621,36 @@ export function tornadoProgram() {
const { note, noteHex, commitmentHex } = deposit;
const encryptedNote = noteAccount
? noteAccount.encryptNote({
address: instanceAddress,
noteHex,
})
: '0x';
const backupFile = `./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`;
console.log(`New deposit: ${deposit.toString()}\n`);
await writeFile(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, {
encoding: 'utf8',
});
console.log(`Writing note backup at ${backupFile}\n`);
await writeFile(backupFile, note, { encoding: 'utf8' });
if (encryptedNote !== '0x') {
console.log(`Storing encrypted note on-chain for backup (Account key: ${accountKey})\n`);
}
await programSendTransaction({
signer,
options,
populatedTransaction: await TornadoProxy.deposit.populateTransaction(instanceAddress, commitmentHex, '0x', {
value: isEth ? denomination : BigInt(0),
}),
populatedTransaction: await TornadoProxy.deposit.populateTransaction(
instanceAddress,
commitmentHex,
encryptedNote,
{
value: isEth ? denomination : BigInt(0),
},
),
});
process.exit(0);
@ -639,10 +669,10 @@ export function tornadoProgram() {
const { currency, amount, netId, commitment } = new Invoice(invoiceString);
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
multicall: multicallAddress,
multicallContract,
routerContract,
nativeCurrency,
tokens: { [currency]: currencyConfig },
@ -673,7 +703,7 @@ export function tornadoProgram() {
}
const TornadoProxy = TornadoRouter__factory.connect(routerContract, signer);
const Multicall = Multicall__factory.connect(multicallAddress, provider);
const Multicall = Multicall__factory.connect(multicallContract, provider);
const Token = tokenAddress ? ERC20__factory.connect(tokenAddress, signer) : undefined;
const [ethBalance, tokenBalance, tokenApprovals] = await multicall(Multicall, [
@ -682,22 +712,20 @@ export function tornadoProgram() {
name: 'getEthBalance',
params: [signer.address],
},
/* eslint-disable prettier/prettier */
...(!isEth
? [
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
: []),
/* eslint-enable prettier/prettier */
]);
if (isEth && denomination > ethBalance) {
@ -756,14 +784,15 @@ export function tornadoProgram() {
const { netId, currency, amount, commitmentHex, nullifierHex, nullifier, secret } = deposit;
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
tornadoSubgraph,
deployedBlock,
nativeCurrency,
multicallContract,
routerContract,
multicall: multicallAddress,
offchainOracleContract,
ovmGasPriceOracleContract,
tokens: { [currency]: currencyConfig },
} = config;
@ -799,17 +828,18 @@ export function tornadoProgram() {
const Tornado = Tornado__factory.connect(instanceAddress, provider);
const TornadoProxy = TornadoRouter__factory.connect(routerContract, !walletWithdrawal ? provider : signer);
const Multicall = Multicall__factory.connect(multicallAddress, provider);
const Multicall = Multicall__factory.connect(multicallContract, provider);
const tornadoFeeOracle = new TornadoFeeOracle(
ovmGasPriceOracleContract
? OvmGasPriceOracle__factory.connect(ovmGasPriceOracleContract, provider)
: undefined,
);
const tokenPriceOracle = new TokenPriceOracle(
provider,
Multicall,
OffchainOracle__factory.connect(TOKEN_PRICE_ORACLE, provider),
offchainOracleContract ? OffchainOracle__factory.connect(offchainOracleContract, provider) : undefined,
);
const depositsServiceConstructor = {
@ -849,9 +879,9 @@ export function tornadoProgram() {
// If we have MERKLE_WORKER_PATH run worker at background otherwise resolve it here
const depositTreeInitiator = await (async () => {
if (MERKLE_WORKER_PATH) {
return () => merkleTreeService.verifyTree({ events: depositEvents }) as Promise<MerkleTree>;
return () => merkleTreeService.verifyTree(depositEvents) as Promise<MerkleTree>;
}
return (await merkleTreeService.verifyTree({ events: depositEvents })) as MerkleTree;
return (await merkleTreeService.verifyTree(depositEvents)) as MerkleTree;
})();
let depositTreePromise: Promise<MerkleTree> | MerkleTree;
@ -916,15 +946,13 @@ export function tornadoProgram() {
readFile(CIRCUIT_PATH, { encoding: 'utf8' }).then((s) => JSON.parse(s)),
readFile(KEY_PATH).then((b) => new Uint8Array(b).buffer),
depositTreePromise,
/* eslint-disable prettier/prettier */
!walletWithdrawal
? getProgramRelayer({
options,
fetchDataOptions,
netId,
}).then(({ relayerClient }) => relayerClient)
options,
fetchDataOptions,
netId,
}).then(({ relayerClient }) => relayerClient)
: undefined,
/* eslint-enable prettier/prettier */
tornadoFeeOracle.fetchL1OptimismFee(),
!isEth ? tokenPriceOracle.fetchPrices([tokenAddress as string]).then((p) => p[0]) : BigInt(0),
provider.getFeeData(),
@ -1118,7 +1146,7 @@ export function tornadoProgram() {
const deposit = await Deposit.parseNote(note);
const { netId, currency, amount, commitmentHex, nullifierHex } = deposit;
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
tornadoSubgraph,
@ -1176,9 +1204,9 @@ export function tornadoProgram() {
// If we have MERKLE_WORKER_PATH run worker at background otherwise resolve it here
const depositTreePromise = await (async () => {
if (MERKLE_WORKER_PATH) {
return () => merkleTreeService.verifyTree({ events: depositEvents }) as Promise<MerkleTree>;
return () => merkleTreeService.verifyTree(depositEvents) as Promise<MerkleTree>;
}
return (await merkleTreeService.verifyTree({ events: depositEvents })) as MerkleTree;
return (await merkleTreeService.verifyTree(depositEvents)) as MerkleTree;
})();
const [withdrawalEvents] = await Promise.all([
@ -1235,32 +1263,31 @@ export function tornadoProgram() {
});
program
.command('syncEvents')
.command('updateEvents')
.description('Sync the local cache file of tornado cash events.\n\n')
.argument('[netId]', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.argument('[currency]', 'Currency to sync events')
.action(
async (
netIdOpts: number | string | undefined,
currencyOpts: string | undefined,
cmdOptions: commonProgramOptions,
) => {
async (netIdOpts: NetIdType | undefined, currencyOpts: string | undefined, cmdOptions: commonProgramOptions) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
const { rpc } = options;
const networks = netIdOpts ? [netIdOpts] : enabledChains;
for (const netId of networks) {
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
tornadoSubgraph,
registrySubgraph,
governanceSubgraph,
tokens,
nativeCurrency,
routerContract,
echoContract,
registryContract,
['governance.contract.tornadocash.eth']: governanceContract,
governanceContract,
deployedBlock,
constants: { GOVERNANCE_BLOCK, REGISTRY_BLOCK, ENCRYPTED_NOTES_BLOCK },
constants: { GOVERNANCE_BLOCK, REGISTRY_BLOCK, NOTE_ACCOUNT_BLOCK, ENCRYPTED_NOTES_BLOCK },
} = config;
const provider = getProgramProvider(netId, rpc, config, {
@ -1272,9 +1299,8 @@ export function tornadoProgram() {
const governanceService = new NodeGovernanceService({
netId,
provider,
// to-do connect governance with subgraph
graphApi: '',
subgraphName: '',
graphApi,
subgraphName: governanceSubgraph,
Governance: Governance__factory.connect(governanceContract, provider),
deployedBlock: GOVERNANCE_BLOCK,
fetchDataOptions,
@ -1301,6 +1327,20 @@ export function tornadoProgram() {
await registryService.updateEvents();
}
const echoService = new NodeEchoService({
netId,
provider,
graphApi,
subgraphName: tornadoSubgraph,
Echoer: Echoer__factory.connect(echoContract, provider),
deployedBlock: NOTE_ACCOUNT_BLOCK,
fetchDataOptions,
cacheDirectory: EVENTS_DIR,
userDirectory: SAVED_DIR,
});
await echoService.updateEvents();
const encryptedNotesService = new NodeEncryptedNotesService({
netId,
provider,
@ -1359,23 +1399,31 @@ export function tornadoProgram() {
merkleWorkerPath: MERKLE_WORKER_PATH,
});
const depositEvents = (await depositsService.updateEvents()).events;
const treeCache = new TreeCache({
netId,
amount,
currency,
userDirectory: SAVED_TREE_DIR,
});
const depositEvents = (await depositsService.updateEvents()).events as DepositsEvents[];
// If we have MERKLE_WORKER_PATH run worker at background otherwise resolve it here
const depositTreePromise = await (async () => {
if (MERKLE_WORKER_PATH) {
return () =>
merkleTreeService.verifyTree({ events: depositEvents as DepositsEvents[] }) as Promise<MerkleTree>;
return () => merkleTreeService.verifyTree(depositEvents) as Promise<MerkleTree>;
}
return (await merkleTreeService.verifyTree({
events: depositEvents as DepositsEvents[],
})) as MerkleTree;
return (await merkleTreeService.verifyTree(depositEvents)) as MerkleTree;
})();
await Promise.all([
withdrawalsService.updateEvents(),
const [tree] = await Promise.all([
typeof depositTreePromise === 'function' ? depositTreePromise() : depositTreePromise,
withdrawalsService.updateEvents(),
]);
if (nativeCurrency === currency) {
await treeCache.createTree(depositEvents, tree);
}
}
}
}
@ -1388,7 +1436,7 @@ export function tornadoProgram() {
.command('relayers')
.description('List all registered relayers from the tornado cash registry.\n\n')
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.action(async (netIdOpts: number | string, cmdOptions: commonProgramOptions) => {
.action(async (netIdOpts: NetIdType, cmdOptions: commonProgramOptions) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
const allRelayers = await getProgramRelayer({
@ -1446,6 +1494,199 @@ export function tornadoProgram() {
process.exit(0);
});
program
.command('createAccount')
.description(
'Creates and save on-chain account that would store encrypted notes. \n\n' +
'Would first lookup on on-chain records to see if the notes are stored. \n\n' +
'Requires a valid signable wallet (mnemonic or a private key) to work (Since they would encrypt or encrypted)',
)
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.action(async (netId: NetIdType, cmdOptions: commonProgramOptions) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
const { rpc } = options;
const config = getConfig(netId);
const {
echoContract,
tornadoSubgraph,
constants: { ['NOTE_ACCOUNT_BLOCK']: deployedBlock },
} = config;
const provider = getProgramProvider(netId, rpc, config, {
...fetchDataOptions,
});
const signer = getProgramSigner({
options,
provider,
});
const graphApi = getProgramGraphAPI(options, config);
if (!signer || signer instanceof VoidSigner) {
throw new Error(
'No wallet found, make your you have supplied a valid mnemonic or private key before using this command',
);
}
/**
* Find for any existing note accounts
*/
const walletPublicKey = NoteAccount.getWalletPublicKey(signer);
const Echoer = Echoer__factory.connect(echoContract, provider);
const newAccount = new NoteAccount({
netId,
Echoer,
});
const echoService = new NodeEchoService({
netId,
provider,
graphApi,
subgraphName: tornadoSubgraph,
Echoer,
deployedBlock,
fetchDataOptions,
cacheDirectory: EVENTS_DIR,
userDirectory: SAVED_DIR,
});
console.log('Getting historic note accounts would take a while\n');
const echoEvents = (await echoService.updateEvents()).events;
const userEvents = echoEvents.filter(({ address }) => address === signer.address);
const existingAccounts = newAccount.decryptAccountsWithWallet(signer, userEvents);
const accountsTable = new Table();
if (existingAccounts.length) {
accountsTable.push(
[{ colSpan: 2, content: `Note Accounts (${netId})`, hAlign: 'center' }],
[{ colSpan: 2, content: `Backed up by: ${signer.address}`, hAlign: 'center' }],
['blockNumber', 'noteAccount'].map((content) => ({ content: colors.red.bold(content) })),
...existingAccounts.map(({ blockNumber, recoveryKey }) => {
return [blockNumber, recoveryKey];
}),
);
console.log(accountsTable.toString() + '\n');
} else {
accountsTable.push(
[{ colSpan: 1, content: `New Note Account (${netId})`, hAlign: 'center' }],
['noteAccount'].map((content) => ({ content: colors.red.bold(content) })),
[newAccount.recoveryKey],
[{ colSpan: 1, content: `Would be backed up by: ${signer.address}`, hAlign: 'center' }],
);
const fileName = `backup-note-account-key-0x${newAccount.recoveryKey.slice(0, 8)}.txt`;
console.log('\n' + accountsTable.toString() + '\n');
console.log(`Writing backup to ${fileName}\n`);
await writeFile(fileName, newAccount.recoveryKey + '\n');
console.log('Backup encrypted account on-chain to use on UI?\n');
await promptConfirmation(options.nonInteractive);
const { data } = newAccount.getEncryptedAccount(walletPublicKey);
console.log('Sending encrypted note account backup transaction through wallet\n');
await programSendTransaction({
signer: signer as TornadoVoidSigner | TornadoWallet,
options,
populatedTransaction: await Echoer.echo.populateTransaction(data),
});
}
process.exit(0);
});
program
.command('decryptNotes')
.description('Fetch notes from deposit events and decrypt them. \n\n' + 'Requires a valid account key to work')
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.argument(
'[accountKey]',
'Account key generated from UI or the createAccount to store encrypted notes on-chain',
parseRecoveryKey,
)
.action(async (netId: NetIdType, accountKey: string | undefined, cmdOptions: commonProgramOptions) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
const { rpc } = options;
if (!accountKey) {
accountKey = options.accountKey;
}
const config = getConfig(netId);
const {
routerContract,
echoContract,
tornadoSubgraph,
constants: { ENCRYPTED_NOTES_BLOCK },
} = config;
const provider = getProgramProvider(netId, rpc, config, {
...fetchDataOptions,
});
const graphApi = getProgramGraphAPI(options, config);
if (!accountKey) {
throw new Error(
'No account key find! Please supply correct account key from either UI or find one with createAccount command',
);
}
const Echoer = Echoer__factory.connect(echoContract, provider);
const noteAccount = new NoteAccount({
netId,
recoveryKey: accountKey,
Echoer,
});
const encryptedNotesService = new NodeEncryptedNotesService({
netId,
provider,
graphApi,
subgraphName: tornadoSubgraph,
Router: TornadoRouter__factory.connect(routerContract, provider),
deployedBlock: ENCRYPTED_NOTES_BLOCK,
fetchDataOptions,
cacheDirectory: EVENTS_DIR,
userDirectory: SAVED_DIR,
});
const encryptedNoteEvents = (await encryptedNotesService.updateEvents()).events;
const accountsTable = new Table();
accountsTable.push(
[{ colSpan: 2, content: `Note Accounts (${netId})`, hAlign: 'center' }],
[{ colSpan: 2, content: `Account key: ${accountKey}`, hAlign: 'center' }],
['blockNumber', 'note'].map((content) => ({ content: colors.red.bold(content) })),
...noteAccount.decryptNotes(encryptedNoteEvents).map(({ blockNumber, address, noteHex }) => {
const { amount, currency } = getInstanceByAddress({ netId, address }) as { amount: string; currency: string };
return [blockNumber, `tornado-${currency}-${amount}-${netId}-${noteHex}`];
}),
);
console.log('\n' + accountsTable.toString() + '\n');
process.exit(0);
});
program
.command('send')
.description('Send ETH or ERC20 token to address.\n\n')
@ -1455,7 +1696,7 @@ export function tornadoProgram() {
.argument('[token]', 'ERC20 Token Contract to check Token Balance', parseAddress)
.action(
async (
netId: string | number,
netId: NetIdType,
to: string,
amountArgs: number | undefined,
tokenArgs: string | undefined,
@ -1464,9 +1705,9 @@ export function tornadoProgram() {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
const { rpc, token: tokenOpts } = options;
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const { currencyName, multicall: multicallAddress } = config;
const { currencyName, multicallContract } = config;
const provider = getProgramProvider(netId, rpc, config, {
...fetchDataOptions,
@ -1482,7 +1723,7 @@ export function tornadoProgram() {
const tokenAddress = tokenArgs ? parseAddress(tokenArgs) : tokenOpts;
const Multicall = Multicall__factory.connect(multicallAddress, provider);
const Multicall = Multicall__factory.connect(multicallContract, provider);
const Token = (tokenAddress ? ERC20__factory.connect(tokenAddress, signer) : undefined) as ERC20;
// Fetching feeData or nonce is unnecessary however we do this to estimate transfer amounts
@ -1508,15 +1749,14 @@ export function tornadoProgram() {
const txGasPrice = feeData.maxFeePerGas
? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0))
: feeData.gasPrice || BigInt(0);
/* eslint-disable prettier/prettier */
const txFees = feeData.maxFeePerGas
? {
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
}
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
}
: {
gasPrice: feeData.gasPrice,
};
gasPrice: feeData.gasPrice,
};
let toSend: bigint;
@ -1543,6 +1783,11 @@ export function tornadoProgram() {
const initCost = txGasPrice * BigInt('400000');
toSend = ethBalance - initCost;
if (ethBalance === BigInt(0) || ethBalance < initCost) {
const errMsg = `Invalid ${currencyName} balance, wants ${formatEther(initCost)} have ${formatEther(ethBalance)}`;
throw new Error(errMsg);
}
const estimatedGas = await provider.estimateGas({
type: txType,
from: signer.address,
@ -1552,10 +1797,10 @@ export function tornadoProgram() {
...txFees,
});
const bumpedGas = (estimatedGas !== BigInt(21000) && signer.gasLimitBump
? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000)
: estimatedGas
);
const bumpedGas =
estimatedGas !== BigInt(21000) && signer.gasLimitBump
? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000)
: estimatedGas;
toSend = ethBalance - txGasPrice * bumpedGas;
}
@ -1567,15 +1812,14 @@ export function tornadoProgram() {
populatedTransaction: tokenAddress
? await Token.transfer.populateTransaction(to, toSend)
: await signer.populateTransaction({
type: txType,
from: signer.address,
to,
value: toSend,
nonce,
...txFees,
}),
type: txType,
from: signer.address,
to,
value: toSend,
nonce,
...txFees,
}),
});
/* eslint-enable prettier/prettier */
process.exit(0);
},
@ -1589,7 +1833,7 @@ export function tornadoProgram() {
.argument('[token]', 'ERC20 Token Contract to check Token Balance', parseAddress)
.action(
async (
netId: string | number,
netId: NetIdType,
addressArgs: string | undefined,
tokenArgs: string | undefined,
cmdOptions: commonProgramOptions,
@ -1597,14 +1841,9 @@ export function tornadoProgram() {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
const { rpc, token: tokenOpts } = options;
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const {
currencyName,
multicall: multicallAddress,
['torn.contract.tornadocash.eth']: tornTokenAddress,
tokens,
} = config;
const { currencyName, multicallContract, tornContract, tokens } = config;
const provider = getProgramProvider(netId, rpc, config, {
...fetchDataOptions,
@ -1617,14 +1856,14 @@ export function tornadoProgram() {
throw new Error('Address is required however no user address is supplied');
}
const Multicall = Multicall__factory.connect(multicallAddress, provider);
const Multicall = Multicall__factory.connect(multicallContract, provider);
const tokenAddresses = Object.values(tokens)
.map(({ tokenAddress }) => tokenAddress)
.filter((t) => t) as string[];
if (tornTokenAddress) {
tokenAddresses.push(tornTokenAddress);
if (tornContract) {
tokenAddresses.push(tornContract);
}
const tokenBalances = await getTokenBalances({
@ -1662,7 +1901,7 @@ export function tornadoProgram() {
const netId = Number(deserializedTx.chainId);
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const provider = getProgramProvider(netId, rpc, config, {
...fetchDataOptions,
@ -1679,7 +1918,6 @@ export function tornadoProgram() {
options,
populatedTransaction: deserializedTx,
});
/* eslint-enable prettier/prettier */
process.exit(0);
});
@ -1698,7 +1936,7 @@ export function tornadoProgram() {
throw new Error('NetId for the transaction is invalid, this command only supports EIP-155 transactions');
}
const config = networkConfig[`netId${netId}`];
const config = getConfig(netId);
const provider = getProgramProvider(netId, rpc, config, {
...fetchDataOptions,
@ -1712,13 +1950,24 @@ export function tornadoProgram() {
});
// common options
/* eslint-disable prettier/prettier */
program.commands.forEach((cmd) => {
cmd.option('-r, --rpc <RPC_URL>', 'The RPC that CLI should interact with', parseUrl);
cmd.option('-e, --eth-rpc <ETHRPC_URL>', 'The Ethereum Mainnet RPC that CLI should interact with', parseUrl);
cmd.option('-g, --graph <GRAPH_URL>', 'The Subgraph API that CLI should interact with', parseUrl);
cmd.option('-G, --eth-graph <ETHGRAPH_URL>', 'The Ethereum Mainnet Subgraph API that CLI should interact with', parseUrl);
cmd.option('-d, --disable-graph', 'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source');
cmd.option(
'-G, --eth-graph <ETHGRAPH_URL>',
'The Ethereum Mainnet Subgraph API that CLI should interact with',
parseUrl,
);
cmd.option(
'-d, --disable-graph',
'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source',
);
cmd.option(
'-a, --account-key <ACCOUNT_KEY>',
'Account key generated from UI or the createAccount to store encrypted notes on-chain',
parseRecoveryKey,
);
cmd.option('-R, --relayer <RELAYER>', 'Withdraw via relayer (Should be either .eth name or URL)', parseRelayer);
cmd.option('-w, --wallet-withdrawal', 'Withdrawal via wallet (Should not be linked with deposits)');
cmd.option('-T, --tor-port <TOR_PORT>', 'Optional tor port', parseNumber);
@ -1730,13 +1979,13 @@ export function tornadoProgram() {
);
cmd.option(
'-m, --mnemonic <MNEMONIC>',
'Wallet BIP39 Mnemonic Phrase - If you didn\'t add it to .env file and it is needed for operation',
'Wallet BIP39 Mnemonic Phrase - If you did not add it to .env file and it is needed for operation',
parseMnemonic,
);
cmd.option('-i, --mnemonic-index <MNEMONIC_INDEX>', 'Optional wallet mnemonic index', parseNumber);
cmd.option(
'-p, --private-key <PRIVATE_KEY>',
'Wallet private key - If you didn\'t add it to .env file and it is needed for operation',
'Wallet private key - If you did not add it to .env file and it is needed for operation',
parseKey,
);
cmd.option(
@ -1745,7 +1994,6 @@ export function tornadoProgram() {
);
cmd.option('-l, --local-rpc', 'Local node mode - Does not submit signed transaction to the node');
});
/* eslint-enable prettier/prettier */
return program;
}

View File

@ -37,22 +37,21 @@ export function unzipAsync(data: Uint8Array): Promise<Unzipped> {
});
}
export async function saveEvents<T extends MinimalEvents>({
name,
export async function saveUserFile({
fileName,
userDirectory,
events,
dataString,
}: {
name: string;
fileName: string;
userDirectory: string;
events: T[];
dataString: string;
}) {
const fileName = `${name}.json`.toLowerCase();
fileName = fileName.toLowerCase();
const filePath = path.join(userDirectory, fileName);
const stringEvents = JSON.stringify(events, null, 2) + '\n';
const payload = await zipAsync({
[fileName]: new TextEncoder().encode(stringEvents),
[fileName]: new TextEncoder().encode(dataString),
});
if (!(await existsAsync(userDirectory))) {
@ -60,7 +59,7 @@ export async function saveEvents<T extends MinimalEvents>({
}
await writeFile(filePath + '.zip', payload);
await writeFile(filePath, stringEvents);
await writeFile(filePath, dataString);
}
export async function loadSavedEvents<T extends MinimalEvents>({

View File

@ -1,10 +1,11 @@
import { bnToBytes, bytesToBN, leBuff2Int, leInt2Buff, rBigInt, toFixedHex } from './utils';
import { buffPedersenHash } from './pedersen';
import type { NetIdType } from './networkConfig';
export type DepositType = {
currency: string;
amount: string;
netId: string | number;
netId: NetIdType;
};
export type createDepositParams = {
@ -61,7 +62,7 @@ export async function createDeposit({ nullifier, secret }: createDepositParams):
export interface DepositConstructor {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
nullifier: bigint;
secret: bigint;
note: string;
@ -74,7 +75,7 @@ export interface DepositConstructor {
export class Deposit {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
nullifier: bigint;
secret: bigint;
@ -148,7 +149,7 @@ export class Deposit {
const newDeposit = new Deposit({
currency: currency.toLowerCase(),
amount: amount,
netId: Number(netId),
netId,
note: `tornado-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.noteHex}`,
noteHex: depositObject.noteHex,
invoice: `tornadoInvoice-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.commitmentHex}`,
@ -182,14 +183,14 @@ export class Deposit {
const invoice = `tornadoInvoice-${currency}-${amount}-${netId}-${depositObject.commitmentHex}`;
const newDeposit = new Deposit({
currency: currency,
amount: amount,
netId: netId,
currency,
amount,
netId,
note: noteString,
noteHex: depositObject.noteHex,
invoice: invoice,
nullifier: nullifier,
secret: secret,
invoice,
nullifier,
secret,
commitmentHex: depositObject.commitmentHex,
nullifierHex: depositObject.nullifierHex,
});
@ -205,7 +206,7 @@ export type parsedInvoiceExec = DepositType & {
export class Invoice {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
commitment: string;
invoice: string;

View File

@ -0,0 +1,189 @@
import { getEncryptionPublicKey, encrypt, decrypt, EthEncryptedData } from '@metamask/eth-sig-util';
import { Echoer } from '@tornado/contracts';
import { Wallet, computeAddress, getAddress } from 'ethers';
import { crypto, base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes } from './utils';
import { EchoEvents, EncryptedNotesEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface NoteToEncrypt {
address: string;
noteHex: string;
}
export interface DecryptedNotes {
blockNumber: number;
address: string;
noteHex: string;
}
export function packEncryptedMessage({ nonce, ephemPublicKey, ciphertext }: EthEncryptedData) {
const nonceBuf = toFixedHex(bytesToHex(base64ToBytes(nonce)), 24);
const ephemPublicKeyBuf = toFixedHex(bytesToHex(base64ToBytes(ephemPublicKey)), 32);
const ciphertextBuf = bytesToHex(base64ToBytes(ciphertext));
const messageBuff = concatBytes(hexToBytes(nonceBuf), hexToBytes(ephemPublicKeyBuf), hexToBytes(ciphertextBuf));
return bytesToHex(messageBuff);
}
export function unpackEncryptedMessage(encryptedMessage: string) {
const messageBuff = hexToBytes(encryptedMessage);
const nonceBuf = bytesToBase64(messageBuff.slice(0, 24));
const ephemPublicKeyBuf = bytesToBase64(messageBuff.slice(24, 56));
const ciphertextBuf = bytesToBase64(messageBuff.slice(56));
return {
messageBuff: bytesToHex(messageBuff),
version: 'x25519-xsalsa20-poly1305',
nonce: nonceBuf,
ephemPublicKey: ephemPublicKeyBuf,
ciphertext: ciphertextBuf,
} as EthEncryptedData & {
messageBuff: string;
};
}
export interface NoteAccountConstructor {
netId: NetIdType;
blockNumber?: number;
// hex
recoveryKey?: string;
Echoer: Echoer;
}
export class NoteAccount {
netId: NetIdType;
blockNumber?: number;
// Dedicated 32 bytes private key only used for note encryption, backed up to an Echoer and local for future derivation
// Note that unlike the private key it shouldn't have the 0x prefix
recoveryKey: string;
// Address derived from recoveryKey, only used for frontend UI
recoveryAddress: string;
// Note encryption public key derived from recoveryKey
recoveryPublicKey: string;
Echoer: Echoer;
constructor({ netId, blockNumber, recoveryKey, Echoer }: NoteAccountConstructor) {
if (!recoveryKey) {
recoveryKey = bytesToHex(crypto.getRandomValues(new Uint8Array(32))).slice(2);
}
this.netId = Math.floor(Number(netId));
this.blockNumber = blockNumber;
this.recoveryKey = recoveryKey;
this.recoveryAddress = computeAddress('0x' + recoveryKey);
this.recoveryPublicKey = getEncryptionPublicKey(recoveryKey);
this.Echoer = Echoer;
}
/**
* Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key
*/
static getWalletPublicKey(wallet: Wallet) {
let { privateKey } = wallet;
if (privateKey.startsWith('0x')) {
privateKey = privateKey.replace('0x', '');
}
// Should return base64 encoded public key
return getEncryptionPublicKey(privateKey);
}
// This function intends to provide an encrypted value of recoveryKey for an on-chain Echoer backup purpose
// Thus, the pubKey should be derived by a Wallet instance or from Web3 wallets
// pubKey: base64 encoded 32 bytes key from https://docs.metamask.io/wallet/reference/eth_getencryptionpublickey/
getEncryptedAccount(walletPublicKey: string) {
const encryptedData = encrypt({
publicKey: walletPublicKey,
data: this.recoveryKey,
version: 'x25519-xsalsa20-poly1305',
});
const data = packEncryptedMessage(encryptedData);
return {
// Use this later to save hexPrivateKey generated with
// Buffer.from(JSON.stringify(encryptedData)).toString('hex')
// As we don't use buffer with this library we should leave UI to do the rest
encryptedData,
// Data that could be used as an echo(data) params
data,
};
}
/**
* Decrypt Echoer backuped note encryption account with private keys
*/
decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[] {
let { privateKey } = wallet;
if (privateKey.startsWith('0x')) {
privateKey = privateKey.replace('0x', '');
}
const decryptedEvents = [];
for (const event of events) {
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount);
const recoveryKey = decrypt({
encryptedData: unpackedMessage,
privateKey,
});
decryptedEvents.push(
new NoteAccount({
netId: this.netId,
blockNumber: event.blockNumber,
recoveryKey,
Echoer: this.Echoer,
}),
);
} catch {
// decryption may fail for invalid accounts
continue;
}
}
return decryptedEvents;
}
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[] {
const decryptedEvents = [];
for (const event of events) {
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedNote);
const [address, noteHex] = decrypt({
encryptedData: unpackedMessage,
privateKey: this.recoveryKey,
}).split('-');
decryptedEvents.push({
blockNumber: event.blockNumber,
address: getAddress(address),
noteHex,
});
} catch {
// decryption may fail for foreign notes
continue;
}
}
return decryptedEvents;
}
encryptNote({ address, noteHex }: NoteToEncrypt) {
const encryptedData = encrypt({
publicKey: this.recoveryPublicKey,
data: `${address}-${noteHex}`,
version: 'x25519-xsalsa20-poly1305',
});
return packEncryptedMessage(encryptedData);
}
}

View File

@ -1,5 +1,12 @@
import { BaseContract, Provider, EventLog, TransactionResponse, getAddress, Block, ContractEventName } from 'ethers';
import type { Tornado, TornadoRouter, TornadoProxyLight, Governance, RelayerRegistry } from '@tornado/contracts';
import type {
Tornado,
TornadoRouter,
TornadoProxyLight,
Governance,
RelayerRegistry,
Echoer,
} from '@tornado/contracts';
import * as graph from '../graphql';
import {
BatchEventsService,
@ -9,25 +16,28 @@ import {
BatchBlockOnProgress,
} from '../batch';
import { fetchDataOptions } from '../providers';
import type { NetIdType } from '../networkConfig';
import type {
BaseEvents,
MinimalEvents,
DepositsEvents,
WithdrawalsEvents,
EncryptedNotesEvents,
AllGovernanceEvents,
GovernanceProposalCreatedEvents,
GovernanceVotedEvents,
GovernanceDelegatedEvents,
GovernanceUndelegatedEvents,
RegistersEvents,
BaseGraphEvents,
EchoEvents,
} from './types';
export const DEPOSIT = 'deposit';
export const WITHDRAWAL = 'withdrawal';
export type BaseEventsServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -57,7 +67,7 @@ export type BaseGraphParams = {
};
export class BaseEventsService<EventType extends MinimalEvents> {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -315,7 +325,7 @@ export class BaseEventsService<EventType extends MinimalEvents> {
}
export type BaseDepositsServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -454,8 +464,78 @@ export class BaseDepositsService extends BaseEventsService<DepositsEvents | With
}
}
export type BaseEchoServiceConstructor = {
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
Echoer: Echoer;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export class BaseEchoService extends BaseEventsService<EchoEvents> {
constructor({
netId,
provider,
graphApi,
subgraphName,
Echoer,
deployedBlock,
fetchDataOptions,
}: BaseEchoServiceConstructor) {
super({ netId, provider, graphApi, subgraphName, contract: Echoer, deployedBlock, fetchDataOptions });
}
getInstanceName(): string {
return `echo_${this.netId}`;
}
getType(): string {
return 'Echo';
}
getGraphMethod(): string {
return 'getAllGraphEchoEvents';
}
async formatEvents(events: EventLog[]) {
return events
.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
const { who, data } = args;
if (who && data) {
const eventObjects = {
blockNumber,
logIndex,
transactionHash,
};
return {
...eventObjects,
address: who,
encryptedAccount: data,
};
}
})
.filter((e) => e) as EchoEvents[];
}
async getEventsFromGraph({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<EchoEvents>> {
// TheGraph doesn't support our batch sync due to missing blockNumber field
if (!this.graphApi || this.graphApi.includes('api.thegraph.com')) {
return {
events: [],
lastBlock: fromBlock,
};
}
return super.getEventsFromGraph({ fromBlock });
}
}
export type BaseEncryptedNotesServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -511,14 +591,8 @@ export class BaseEncryptedNotesService extends BaseEventsService<EncryptedNotesE
}
}
export type BaseGovernanceEventTypes =
| GovernanceProposalCreatedEvents
| GovernanceVotedEvents
| GovernanceDelegatedEvents
| GovernanceUndelegatedEvents;
export type BaseGovernanceServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;
@ -527,7 +601,7 @@ export type BaseGovernanceServiceConstructor = {
fetchDataOptions?: fetchDataOptions;
};
export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEventTypes> {
export class BaseGovernanceService extends BaseEventsService<AllGovernanceEvents> {
batchTransactionService: BatchTransactionService;
constructor({
@ -556,70 +630,71 @@ export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEvent
}
getGraphMethod() {
return 'governanceEvents';
return 'getAllGovernanceEvents';
}
async formatEvents(events: EventLog[]): Promise<BaseGovernanceEventTypes[]> {
const formattedEvents = events
.map(({ blockNumber, index: logIndex, transactionHash, args, eventName: event }) => {
const eventObjects = {
blockNumber,
logIndex,
transactionHash,
event,
};
async formatEvents(events: EventLog[]): Promise<AllGovernanceEvents[]> {
const proposalEvents: GovernanceProposalCreatedEvents[] = [];
const votedEvents: GovernanceVotedEvents[] = [];
const delegatedEvents: GovernanceDelegatedEvents[] = [];
const undelegatedEvents: GovernanceUndelegatedEvents[] = [];
if (event === 'ProposalCreated') {
const { id, proposer, target, startTime, endTime, description } = args;
return {
...eventObjects,
id,
proposer,
target,
startTime,
endTime,
description,
} as GovernanceProposalCreatedEvents;
}
events.forEach(({ blockNumber, index: logIndex, transactionHash, args, eventName: event }) => {
const eventObjects = {
blockNumber,
logIndex,
transactionHash,
event,
};
if (event === 'Voted') {
const { proposalId, voter, support, votes } = args;
return {
...eventObjects,
proposalId,
voter,
support,
votes,
} as GovernanceVotedEvents;
}
if (event === 'ProposalCreated') {
const { id, proposer, target, startTime, endTime, description } = args;
if (event === 'Delegated') {
const { account, to: delegateTo } = args;
return {
...eventObjects,
account,
delegateTo,
} as GovernanceDelegatedEvents;
}
proposalEvents.push({
...eventObjects,
id: Number(id),
proposer,
target,
startTime: Number(startTime),
endTime: Number(endTime),
description,
});
}
if (event === 'Undelegated') {
const { account, from: delegateFrom } = args;
return {
...eventObjects,
account,
delegateFrom,
} as GovernanceUndelegatedEvents;
}
})
.filter((e) => e) as BaseGovernanceEventTypes[];
if (event === 'Voted') {
const { proposalId, voter, support, votes } = args;
type GovernanceVotedEventsIndexed = GovernanceVotedEvents & {
index: number;
};
votedEvents.push({
...eventObjects,
proposalId: Number(proposalId),
voter,
support,
votes,
from: '',
input: '',
});
}
const votedEvents = formattedEvents
.map((event, index) => ({ ...event, index }))
.filter(({ event }) => event === 'Voted') as GovernanceVotedEventsIndexed[];
if (event === 'Delegated') {
const { account, to: delegateTo } = args;
delegatedEvents.push({
...eventObjects,
account,
delegateTo,
});
}
if (event === 'Undelegated') {
const { account, from: delegateFrom } = args;
undelegatedEvents.push({
...eventObjects,
account,
delegateFrom,
});
}
});
if (votedEvents.length) {
this.updateTransactionProgress({ percentage: 0 });
@ -628,7 +703,7 @@ export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEvent
...new Set(votedEvents.map(({ transactionHash }) => transactionHash)),
]);
votedEvents.forEach((event) => {
votedEvents.forEach((event, index) => {
// eslint-disable-next-line prefer-const
let { data: input, from } = txs.find((t) => t.hash === event.transactionHash) as TransactionResponse;
@ -637,19 +712,17 @@ export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEvent
input = '';
}
// @ts-expect-error check formattedEvents types later
formattedEvents[event.index].from = from;
// @ts-expect-error check formattedEvents types later
formattedEvents[event.index].input = input;
votedEvents[index].from = from;
votedEvents[index].input = input;
});
}
return formattedEvents;
return [...proposalEvents, ...votedEvents, ...delegatedEvents, ...undelegatedEvents];
}
async getEventsFromGraph({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<BaseGovernanceEventTypes>> {
async getEventsFromGraph({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<AllGovernanceEvents>> {
// TheGraph doesn't support governance subgraphs
if (!this.graphApi || this.graphApi.includes('api.thegraph.com')) {
if (!this.graphApi || !this.subgraphName || this.graphApi.includes('api.thegraph.com')) {
return {
events: [],
lastBlock: fromBlock,
@ -661,7 +734,7 @@ export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEvent
}
export type BaseRegistryServiceConstructor = {
netId: number | string;
netId: NetIdType;
provider: Provider;
graphApi?: string;
subgraphName?: string;

View File

@ -1,7 +1,7 @@
import Table from 'cli-table3';
import moment from 'moment';
import { BatchBlockOnProgress, BatchEventOnProgress } from '../batch';
import { saveEvents, loadSavedEvents, loadCachedEvents } from '../data';
import { saveUserFile, loadSavedEvents, loadCachedEvents } from '../data';
import {
BaseDepositsService,
BaseEncryptedNotesService,
@ -11,9 +11,18 @@ import {
BaseEncryptedNotesServiceConstructor,
BaseGovernanceServiceConstructor,
BaseRegistryServiceConstructor,
BaseGovernanceEventTypes,
BaseEchoServiceConstructor,
BaseEchoService,
} from './base';
import type { BaseEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, RegistersEvents } from './types';
import type {
BaseEvents,
DepositsEvents,
WithdrawalsEvents,
EncryptedNotesEvents,
RegistersEvents,
AllGovernanceEvents,
EchoEvents,
} from './types';
export type NodeDepositsServiceConstructor = BaseDepositsServiceConstructor & {
cacheDirectory?: string;
@ -175,10 +184,155 @@ export class NodeDepositsService extends BaseDepositsService {
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<DepositsEvents | WithdrawalsEvents>({
name: instanceName,
await saveUserFile({
fileName: instanceName + '.json',
userDirectory: this.userDirectory,
events,
dataString: JSON.stringify(events, null, 2) + '\n',
});
}
}
}
export type NodeEchoServiceConstructor = BaseEchoServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
};
export class NodeEchoService extends BaseEchoService {
cacheDirectory?: string;
userDirectory?: string;
constructor({
netId,
provider,
graphApi,
subgraphName,
Echoer,
deployedBlock,
fetchDataOptions,
cacheDirectory,
userDirectory,
}: NodeEchoServiceConstructor) {
super({
netId,
provider,
graphApi,
subgraphName,
Echoer,
deployedBlock,
fetchDataOptions,
});
this.cacheDirectory = cacheDirectory;
this.userDirectory = userDirectory;
}
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
}
}
}
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events from graph node count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
}
}
}
async getEventsFromDB() {
if (!this.userDirectory) {
console.log(`Updating events for ${this.netId} chain echo events\n`);
console.log(`savedEvents count - ${0}`);
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const savedEvents = await loadSavedEvents<EchoEvents>({
name: this.getInstanceName(),
userDirectory: this.userDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`Updating events for ${this.netId} chain echo events\n`);
console.log(`savedEvents count - ${savedEvents.events.length}`);
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
return savedEvents;
}
async getEventsFromCache() {
if (!this.cacheDirectory) {
console.log(`cachedEvents count - ${0}`);
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const cachedEvents = await loadCachedEvents<EchoEvents>({
name: this.getInstanceName(),
cacheDirectory: this.cacheDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
return cachedEvents;
}
async saveEvents({ events, lastBlock }: BaseEvents<EchoEvents>) {
const instanceName = this.getInstanceName();
console.log('\ntotalEvents count - ', events.length);
console.log(
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
);
const eventTable = new Table();
eventTable.push(
[{ colSpan: 2, content: 'Echo Accounts', hAlign: 'center' }],
['Network', `${this.netId} chain`],
['Events', `${events.length} events`],
[{ colSpan: 2, content: 'Latest events' }],
...events
.slice(events.length - 10)
.reverse()
.map(({ blockNumber }, index) => {
const eventIndex = events.length - index;
return [eventIndex, blockNumber];
}),
);
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveUserFile({
fileName: instanceName + '.json',
userDirectory: this.userDirectory,
dataString: JSON.stringify(events, null, 2) + '\n',
});
}
}
@ -320,10 +474,10 @@ export class NodeEncryptedNotesService extends BaseEncryptedNotesService {
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<EncryptedNotesEvents>({
name: instanceName,
await saveUserFile({
fileName: instanceName + '.json',
userDirectory: this.userDirectory,
events,
dataString: JSON.stringify(events, null, 2) + '\n',
});
}
}
@ -407,7 +561,7 @@ export class NodeGovernanceService extends BaseGovernanceService {
};
}
const savedEvents = await loadSavedEvents<BaseGovernanceEventTypes>({
const savedEvents = await loadSavedEvents<AllGovernanceEvents>({
name: this.getInstanceName(),
userDirectory: this.userDirectory,
deployedBlock: this.deployedBlock,
@ -431,7 +585,7 @@ export class NodeGovernanceService extends BaseGovernanceService {
};
}
const cachedEvents = await loadCachedEvents<BaseGovernanceEventTypes>({
const cachedEvents = await loadCachedEvents<AllGovernanceEvents>({
name: this.getInstanceName(),
cacheDirectory: this.cacheDirectory,
deployedBlock: this.deployedBlock,
@ -443,7 +597,7 @@ export class NodeGovernanceService extends BaseGovernanceService {
return cachedEvents;
}
async saveEvents({ events, lastBlock }: BaseEvents<BaseGovernanceEventTypes>) {
async saveEvents({ events, lastBlock }: BaseEvents<AllGovernanceEvents>) {
const instanceName = this.getInstanceName();
console.log('\ntotalEvents count - ', events.length);
@ -471,10 +625,10 @@ export class NodeGovernanceService extends BaseGovernanceService {
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<BaseGovernanceEventTypes>({
name: instanceName,
await saveUserFile({
fileName: instanceName + '.json',
userDirectory: this.userDirectory,
events,
dataString: JSON.stringify(events, null, 2) + '\n',
});
}
}
@ -616,10 +770,10 @@ export class NodeRegistryService extends BaseRegistryService {
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<RegistersEvents>({
name: instanceName,
await saveUserFile({
fileName: instanceName + '.json',
userDirectory: this.userDirectory,
events,
dataString: JSON.stringify(events, null, 2) + '\n',
});
}
}

View File

@ -48,6 +48,12 @@ export type GovernanceUndelegatedEvents = GovernanceEvents & {
delegateFrom: string;
};
export type AllGovernanceEvents =
| GovernanceProposalCreatedEvents
| GovernanceVotedEvents
| GovernanceDelegatedEvents
| GovernanceUndelegatedEvents;
export type RegistersEvents = MinimalEvents & RelayerParams;
export type DepositsEvents = MinimalEvents & {
@ -64,6 +70,11 @@ export type WithdrawalsEvents = MinimalEvents & {
timestamp: number;
};
export type EchoEvents = MinimalEvents & {
address: string;
encryptedAccount: string;
};
export type EncryptedNotesEvents = MinimalEvents & {
encryptedNote: string;
};

View File

@ -8,6 +8,12 @@ import type {
WithdrawalsEvents,
EncryptedNotesEvents,
BatchGraphOnProgress,
EchoEvents,
AllGovernanceEvents,
GovernanceProposalCreatedEvents,
GovernanceVotedEvents,
GovernanceDelegatedEvents,
GovernanceUndelegatedEvents,
} from '../events';
import {
_META,
@ -17,6 +23,8 @@ import {
GET_WITHDRAWALS,
GET_NOTE_ACCOUNTS,
GET_ENCRYPTED_NOTES,
GET_ECHO_EVENTS,
GET_GOVERNANCE_EVENTS,
} from './queries';
export * from './queries';
@ -643,7 +651,7 @@ export async function getNoteAccounts({
subgraphName,
query: GET_NOTE_ACCOUNTS,
variables: {
address,
address: address.toLowerCase(),
},
fetchDataOptions,
});
@ -662,6 +670,132 @@ export async function getNoteAccounts({
}
}
export interface GraphEchoEvents {
noteAccounts: {
id: string;
blockNumber: string;
address: string;
encryptedAccount: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getGraphEchoEventsParams {
graphApi: string;
subgraphName: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export function getGraphEchoEvents({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
}: getGraphEchoEventsParams): Promise<GraphEchoEvents> {
return queryGraph<GraphEchoEvents>({
graphApi,
subgraphName,
query: GET_ECHO_EVENTS,
variables: {
first,
fromBlock,
},
fetchDataOptions,
});
}
export async function getAllGraphEchoEvents({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
onProgress,
}: getGraphEchoEventsParams): Promise<BaseGraphEvents<EchoEvents>> {
try {
const events = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
let {
noteAccounts: result,
_meta: {
// eslint-disable-next-line prefer-const
block: { number: currentBlock },
},
} = await getGraphEchoEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions });
lastSyncBlock = currentBlock;
if (isEmptyArray(result)) {
break;
}
const [firstEvent] = result;
const [lastEvent] = result.slice(-1);
if (typeof onProgress === 'function') {
onProgress({
type: 'EchoEvents',
fromBlock: Number(firstEvent.blockNumber),
toBlock: Number(lastEvent.blockNumber),
count: result.length,
});
}
if (result.length < 900) {
events.push(...result);
break;
}
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
fromBlock = Number(lastEvent.blockNumber);
events.push(...result);
}
if (!events.length) {
return {
events: [],
lastSyncBlock,
};
}
const result = events.map((e) => {
const [transactionHash, logIndex] = e.id.split('-');
return {
blockNumber: Number(e.blockNumber),
logIndex: Number(logIndex),
transactionHash: transactionHash,
address: getAddress(e.address),
encryptedAccount: e.encryptedAccount,
};
});
const [lastEvent] = result.slice(-1);
return {
events: result,
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
};
} catch (err) {
console.log('Error from getAllGraphEchoEvents query');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
}
}
export interface GraphEncryptedNotes {
encryptedNotes: {
blockNumber: string;
@ -782,3 +916,223 @@ export async function getAllEncryptedNotes({
};
}
}
export interface GraphGovernanceEvents {
proposals: {
blockNumber: number;
logIndex: number;
transactionHash: string;
proposalId: number;
proposer: string;
target: string;
startTime: number;
endTime: number;
description: string;
}[];
votes: {
blockNumber: number;
logIndex: number;
transactionHash: string;
proposalId: number;
voter: string;
support: boolean;
votes: string;
from: string;
input: string;
}[];
delegates: {
blockNumber: number;
logIndex: number;
transactionHash: string;
account: string;
delegateTo: string;
}[];
undelegates: {
blockNumber: number;
logIndex: number;
transactionHash: string;
account: string;
delegateFrom: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getGovernanceEventsParams {
graphApi: string;
subgraphName: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export function getGovernanceEvents({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
}: getGovernanceEventsParams): Promise<GraphGovernanceEvents> {
return queryGraph<GraphGovernanceEvents>({
graphApi,
subgraphName,
query: GET_GOVERNANCE_EVENTS,
variables: {
first,
fromBlock,
},
fetchDataOptions,
});
}
export async function getAllGovernanceEvents({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
onProgress,
}: getGovernanceEventsParams): Promise<BaseGraphEvents<AllGovernanceEvents>> {
try {
const result = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
const {
proposals,
votes,
delegates,
undelegates,
_meta: {
block: { number: currentBlock },
},
} = await getGovernanceEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions });
lastSyncBlock = currentBlock;
const eventsLength = proposals.length + votes.length + delegates.length + undelegates.length;
if (eventsLength === 0) {
break;
}
const formattedProposals: GovernanceProposalCreatedEvents[] = proposals.map(
({ blockNumber, logIndex, transactionHash, proposalId, proposer, target, startTime, endTime, description }) => {
return {
blockNumber: Number(blockNumber),
logIndex: Number(logIndex),
transactionHash,
event: 'ProposalCreated',
id: Number(proposalId),
proposer: getAddress(proposer),
target: getAddress(target),
startTime: Number(startTime),
endTime: Number(endTime),
description,
};
},
);
const formattedVotes: GovernanceVotedEvents[] = votes.map(
({ blockNumber, logIndex, transactionHash, proposalId, voter, support, votes, from, input }) => {
// Filter spammy txs
if (!input || input.length > 2048) {
input = '';
}
return {
blockNumber: Number(blockNumber),
logIndex: Number(logIndex),
transactionHash,
event: 'Voted',
proposalId: Number(proposalId),
voter: getAddress(voter),
support,
votes,
from: getAddress(from),
input,
};
},
);
const formattedDelegates: GovernanceDelegatedEvents[] = delegates.map(
({ blockNumber, logIndex, transactionHash, account, delegateTo }) => {
return {
blockNumber: Number(blockNumber),
logIndex: Number(logIndex),
transactionHash,
event: 'Delegated',
account: getAddress(account),
delegateTo: getAddress(delegateTo),
};
},
);
const formattedUndelegates: GovernanceUndelegatedEvents[] = undelegates.map(
({ blockNumber, logIndex, transactionHash, account, delegateFrom }) => {
return {
blockNumber: Number(blockNumber),
logIndex: Number(logIndex),
transactionHash,
event: 'Undelegated',
account: getAddress(account),
delegateFrom: getAddress(delegateFrom),
};
},
);
let formattedEvents = [
...formattedProposals,
...formattedVotes,
...formattedDelegates,
...formattedUndelegates,
].sort((a, b) => {
if (a.blockNumber === b.blockNumber) {
return a.logIndex - b.logIndex;
}
return a.blockNumber - b.blockNumber;
});
if (eventsLength < 900) {
result.push(...formattedEvents);
break;
}
const [firstEvent] = formattedEvents;
const [lastEvent] = formattedEvents.slice(-1);
if (typeof onProgress === 'function') {
onProgress({
type: 'Governance Events',
fromBlock: Number(firstEvent.blockNumber),
toBlock: Number(lastEvent.blockNumber),
count: eventsLength,
});
}
formattedEvents = formattedEvents.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
fromBlock = Number(lastEvent.blockNumber);
result.push(...formattedEvents);
}
const [lastEvent] = result.slice(-1);
return {
events: result,
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
};
} catch (err) {
console.log('Error from getAllGovernance query');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
}
}

View File

@ -107,6 +107,23 @@ export const GET_NOTE_ACCOUNTS = `
}
`;
export const GET_ECHO_EVENTS = `
query getNoteAccounts($first: Int, $fromBlock: Int) {
noteAccounts(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
id
blockNumber
address
encryptedAccount
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_ENCRYPTED_NOTES = `
query getEncryptedNotes($first: Int, $fromBlock: Int) {
encryptedNotes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
@ -123,3 +140,58 @@ export const GET_ENCRYPTED_NOTES = `
}
}
`;
export const GET_GOVERNANCE_EVENTS = `
query getGovernanceEvents($first: Int, $fromBlock: Int) {
proposals(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
blockNumber
logIndex
transactionHash
proposalId
proposer
target
startTime
endTime
description
}
votes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
blockNumber
logIndex
transactionHash
proposalId
voter
support
votes
from
input
}
delegates(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
blockNumber
logIndex
transactionHash
account
delegateTo
}
undelegates(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
blockNumber
logIndex
transactionHash
account
delegateFrom
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_GOVERNANCE_APY = `
stakeDailyBurns(first: 30, orderBy: date, orderDirection: desc) {
id
date
dailyAmountBurned
}
`;

View File

@ -4,6 +4,7 @@ export * from './schemas';
export * from './batch';
export * from './data';
export * from './deposits';
export * from './encryptedNotes';
export * from './fees';
export * from './merkleTree';
export * from './mimc';
@ -15,5 +16,6 @@ export * from './prices';
export * from './providers';
export * from './relayerClient';
export * from './tokens';
export * from './treeCache';
export * from './utils';
export * from './websnark';

View File

@ -1,14 +1,15 @@
import { Worker as NodeWorker } from 'worker_threads';
import { MerkleTree, Element } from '@tornado/fixed-merkle-tree';
import { MerkleTree, PartialMerkleTree, Element, TreeEdge } from '@tornado/fixed-merkle-tree';
import type { Tornado } from '@tornado/contracts';
import { isNode, toFixedHex } from './utils';
import { mimc } from './mimc';
import type { DepositType } from './deposits';
import type { DepositsEvents } from './events';
import type { NetIdType } from './networkConfig';
export type MerkleTreeConstructor = DepositType & {
Tornado: Tornado;
commitment?: string;
commitmentHex?: string;
merkleTreeHeight?: number;
emptyElement?: string;
merkleWorkerPath?: string;
@ -17,9 +18,9 @@ export type MerkleTreeConstructor = DepositType & {
export class MerkleTreeService {
currency: string;
amount: string;
netId: number;
netId: NetIdType;
Tornado: Tornado;
commitment?: string;
commitmentHex?: string;
instanceName: string;
merkleTreeHeight: number;
@ -32,7 +33,7 @@ export class MerkleTreeService {
amount,
currency,
Tornado,
commitment,
commitmentHex,
merkleTreeHeight = 20,
emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292',
merkleWorkerPath,
@ -45,14 +46,14 @@ export class MerkleTreeService {
this.Tornado = Tornado;
this.instanceName = instanceName;
this.commitment = commitment;
this.commitmentHex = commitmentHex;
this.merkleTreeHeight = merkleTreeHeight;
this.emptyElement = emptyElement;
this.merkleWorkerPath = merkleWorkerPath;
}
async createTree({ events }: { events: Element[] }) {
async createTree(events: Element[]) {
const { hash: hashFunction } = await mimc.getHash();
if (this.merkleWorkerPath) {
@ -113,13 +114,76 @@ export class MerkleTreeService {
});
}
async verifyTree({ events }: { events: DepositsEvents[] }) {
async createPartialTree({ edge, elements }: { edge: TreeEdge; elements: Element[] }) {
const { hash: hashFunction } = await mimc.getHash();
if (this.merkleWorkerPath) {
console.log('Using merkleWorker\n');
try {
if (isNode) {
const merkleWorkerPromise = new Promise((resolve, reject) => {
const worker = new NodeWorker(this.merkleWorkerPath as string, {
workerData: {
merkleTreeHeight: this.merkleTreeHeight,
edge,
elements,
zeroElement: this.emptyElement,
},
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
}) as Promise<string>;
return PartialMerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction);
} else {
const merkleWorkerPromise = new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const worker = new (Worker as any)(this.merkleWorkerPath);
worker.onmessage = (e: { data: string }) => {
resolve(e.data);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
worker.onerror = (e: any) => {
reject(e);
};
worker.postMessage({
merkleTreeHeight: this.merkleTreeHeight,
edge,
elements,
zeroElement: this.emptyElement,
});
}) as Promise<string>;
return PartialMerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction);
}
} catch (err) {
console.log('merkleWorker failed, falling back to synchronous merkle tree');
console.log(err);
}
}
return new PartialMerkleTree(this.merkleTreeHeight, edge, elements, {
zeroElement: this.emptyElement,
hashFunction,
});
}
async verifyTree(events: DepositsEvents[]) {
console.log(
`\nCreating deposit tree for ${this.netId} ${this.amount} ${this.currency.toUpperCase()} would take a while\n`,
);
console.time('Created tree in');
const tree = await this.createTree({ events: events.map(({ commitment }) => BigInt(commitment).toString()) });
const tree = await this.createTree(events.map(({ commitment }) => commitment));
console.timeEnd('Created tree in');
console.log('');

View File

@ -1,3 +1,19 @@
/**
* Type of default supported networks
*/
export enum NetId {
MAINNET = 1,
BSC = 56,
POLYGON = 137,
OPTIMISM = 10,
ARBITRUM = 42161,
GNOSIS = 100,
AVALANCHE = 43114,
SEPOLIA = 11155111,
}
export type NetIdType = NetId | number;
export interface RpcUrl {
name: string;
url: string;
@ -46,20 +62,20 @@ export type Config = {
};
nativeCurrency: string;
currencyName: string;
explorerUrl: {
tx: string;
address: string;
block: string;
};
explorerUrl: string;
merkleTreeHeight: number;
emptyElement: string;
networkName: string;
deployedBlock: number;
rpcUrls: RpcUrls;
multicall: string;
multicallContract: string;
routerContract: string;
registryContract?: string;
echoContract: string;
offchainOracleContract?: string;
tornContract?: string;
governanceContract?: string;
stakingRewardsContract?: string;
registryContract?: string;
aggregatorContract?: string;
reverseRecordsContract?: string;
gasPriceOracleContract?: string;
@ -67,6 +83,7 @@ export type Config = {
ovmGasPriceOracleContract?: string;
tornadoSubgraph: string;
registrySubgraph?: string;
governanceSubgraph?: string;
subgraphs: SubgraphUrls;
tokens: TokenInstances;
optionalTokens?: string[];
@ -81,20 +98,12 @@ export type Config = {
// Should be in seconds
MINING_BLOCK_TIME?: number;
};
'torn.contract.tornadocash.eth'?: string;
'governance.contract.tornadocash.eth'?: string;
'staking-rewards.contract.tornadocash.eth'?: string;
'tornado-router.contract.tornadocash.eth'?: string;
'tornado-proxy-light.contract.tornadocash.eth'?: string;
};
export type networkConfig = {
[key in string]: Config;
[key in NetIdType]: Config;
};
export const blockSyncInterval = 10000;
export const enabledChains = ['1', '10', '56', '100', '137', '42161', '43114', '11155111'];
const theGraph = {
name: 'Hosted Graph',
url: 'https://api.thegraph.com',
@ -104,8 +113,8 @@ const tornado = {
url: 'https://tornadocash-rpc.com',
};
export const networkConfig: networkConfig = {
netId1: {
export const defaultConfig: networkConfig = {
[NetId.MAINNET]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 80,
@ -115,11 +124,7 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'eth',
currencyName: 'ETH',
explorerUrl: {
tx: 'https://etherscan.io/tx/',
address: 'https://etherscan.io/address/',
block: 'https://etherscan.io/block/',
},
explorerUrl: 'https://etherscan.io',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Ethereum Mainnet',
@ -130,7 +135,7 @@ export const networkConfig: networkConfig = {
url: 'https://tornadocash-rpc.com',
},
chainnodes: {
name: 'Tornado RPC',
name: 'Chainnodes RPC',
url: 'https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
mevblockerRPC: {
@ -158,14 +163,19 @@ export const networkConfig: networkConfig = {
url: 'https://1rpc.io/eth',
},
},
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
registryContract: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2',
echoContract: '0x9B27DD5Bb15d42DC224FCD0B7caEbBe16161Df42',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
tornContract: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
governanceContract: '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
stakingRewardsContract: '0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29',
registryContract: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2',
aggregatorContract: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49',
reverseRecordsContract: '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C',
tornadoSubgraph: 'tornadocash/mainnet-tornado-subgraph',
registrySubgraph: 'tornadocash/tornado-relayer-registry',
governanceSubgraph: 'tornadocash/tornado-governance',
subgraphs: {
tornado,
theGraph,
@ -247,16 +257,12 @@ export const networkConfig: networkConfig = {
constants: {
GOVERNANCE_BLOCK: 11474695,
NOTE_ACCOUNT_BLOCK: 11842486,
ENCRYPTED_NOTES_BLOCK: 14248730,
ENCRYPTED_NOTES_BLOCK: 12143762,
REGISTRY_BLOCK: 14173129,
MINING_BLOCK_TIME: 15,
},
'torn.contract.tornadocash.eth': '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
'governance.contract.tornadocash.eth': '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
'tornado-router.contract.tornadocash.eth': '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
'staking-rewards.contract.tornadocash.eth': '0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29',
},
netId56: {
[NetId.BSC]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 5,
@ -266,18 +272,15 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'bnb',
currencyName: 'BNB',
explorerUrl: {
tx: 'https://bscscan.com/tx/',
address: 'https://bscscan.com/address/',
block: 'https://bscscan.com/block/',
},
explorerUrl: 'https://bscscan.com',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Binance Smart Chain',
deployedBlock: 8158799,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
tornadoSubgraph: 'tornadocash/bsc-tornado-subgraph',
subgraphs: {
tornado,
@ -289,7 +292,7 @@ export const networkConfig: networkConfig = {
url: 'https://tornadocash-rpc.com/bsc',
},
chainnodes: {
name: 'Tornado RPC',
name: 'Chainnodes RPC',
url: 'https://bsc-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
stackup: {
@ -323,9 +326,8 @@ export const networkConfig: networkConfig = {
NOTE_ACCOUNT_BLOCK: 8159269,
ENCRYPTED_NOTES_BLOCK: 8159269,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId137: {
[NetId.POLYGON]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 100,
@ -335,18 +337,15 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'matic',
currencyName: 'MATIC',
explorerUrl: {
tx: 'https://polygonscan.com/tx/',
address: 'https://polygonscan.com/address/',
block: 'https://polygonscan.com/block/',
},
explorerUrl: 'https://polygonscan.com',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Polygon (Matic) Network',
deployedBlock: 16257962,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
gasPriceOracleContract: '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C',
tornadoSubgraph: 'tornadocash/matic-tornado-subgraph',
subgraphs: {
@ -385,9 +384,8 @@ export const networkConfig: networkConfig = {
NOTE_ACCOUNT_BLOCK: 16257996,
ENCRYPTED_NOTES_BLOCK: 16257996,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId10: {
[NetId.OPTIMISM]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 0.001,
@ -397,18 +395,15 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'eth',
currencyName: 'ETH',
explorerUrl: {
tx: 'https://optimistic.etherscan.io/tx/',
address: 'https://optimistic.etherscan.io/address/',
block: 'https://optimistic.etherscan.io/block/',
},
explorerUrl: 'https://optimistic.etherscan.io',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Optimism',
deployedBlock: 2243689,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
ovmGasPriceOracleContract: '0x420000000000000000000000000000000000000F',
tornadoSubgraph: 'tornadocash/optimism-tornado-subgraph',
subgraphs: {
@ -421,7 +416,7 @@ export const networkConfig: networkConfig = {
url: 'https://tornadocash-rpc.com/op',
},
chainnodes: {
name: 'Tornado RPC',
name: 'Chainnodes RPC',
url: 'https://optimism-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
optimism: {
@ -455,9 +450,8 @@ export const networkConfig: networkConfig = {
NOTE_ACCOUNT_BLOCK: 2243694,
ENCRYPTED_NOTES_BLOCK: 2243694,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId42161: {
[NetId.ARBITRUM]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 4,
@ -467,18 +461,15 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'eth',
currencyName: 'ETH',
explorerUrl: {
tx: 'https://arbiscan.io/tx/',
address: 'https://arbiscan.io/address/',
block: 'https://arbiscan.io/block/',
},
explorerUrl: 'https://arbiscan.io',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Arbitrum One',
deployedBlock: 3430648,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
tornadoSubgraph: 'tornadocash/arbitrum-tornado-subgraph',
subgraphs: {
tornado,
@ -490,7 +481,7 @@ export const networkConfig: networkConfig = {
url: 'https://tornadocash-rpc.com/arbitrum',
},
chainnodes: {
name: 'Tornado RPC',
name: 'Chainnodes RPC',
url: 'https://arbitrum-one.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
arbitrum: {
@ -524,9 +515,8 @@ export const networkConfig: networkConfig = {
NOTE_ACCOUNT_BLOCK: 3430605,
ENCRYPTED_NOTES_BLOCK: 3430605,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId100: {
[NetId.GNOSIS]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 6,
@ -536,18 +526,15 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'xdai',
currencyName: 'xDAI',
explorerUrl: {
tx: 'https://blockscout.com/xdai/mainnet/tx/',
address: 'https://blockscout.com/xdai/mainnet/address/',
block: 'https://blockscout.com/xdai/mainnet/block/',
},
explorerUrl: 'https://gnosisscan.io',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Gnosis Chain',
deployedBlock: 17754561,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
tornadoSubgraph: 'tornadocash/xdai-tornado-subgraph',
subgraphs: {
tornado,
@ -559,7 +546,7 @@ export const networkConfig: networkConfig = {
url: 'https://tornadocash-rpc.com/gnosis',
},
chainnodes: {
name: 'Tornado RPC',
name: 'Chainnodes RPC',
url: 'https://gnosis-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
gnosis: {
@ -593,9 +580,8 @@ export const networkConfig: networkConfig = {
NOTE_ACCOUNT_BLOCK: 17754564,
ENCRYPTED_NOTES_BLOCK: 17754564,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId43114: {
[NetId.AVALANCHE]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 225,
@ -605,18 +591,15 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'avax',
currencyName: 'AVAX',
explorerUrl: {
tx: 'https://snowtrace.io/tx/',
address: 'https://snowtrace.io/address/',
block: 'https://snowtrace.io/block/',
},
explorerUrl: 'https://snowtrace.io',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Avalanche Mainnet',
deployedBlock: 4429818,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
tornadoSubgraph: 'tornadocash/avalanche-tornado-subgraph',
subgraphs: {
theGraph,
@ -652,9 +635,8 @@ export const networkConfig: networkConfig = {
NOTE_ACCOUNT_BLOCK: 4429813,
ENCRYPTED_NOTES_BLOCK: 4429813,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId11155111: {
[NetId.SEPOLIA]: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 2,
@ -664,19 +646,18 @@ export const networkConfig: networkConfig = {
},
nativeCurrency: 'eth',
currencyName: 'SepoliaETH',
explorerUrl: {
tx: 'https://sepolia.etherscan.io/tx/',
address: 'https://sepolia.etherscan.io/address/',
block: 'https://sepolia.etherscan.io/block/',
},
explorerUrl: 'https://sepolia.etherscan.io',
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Ethereum Sepolia',
deployedBlock: 5594395,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
tornContract: '0x3AE6667167C0f44394106E197904519D808323cA',
governanceContract: '0xe5324cD7602eeb387418e594B87aCADee08aeCAD',
stakingRewardsContract: '0x6d0018890751Efd31feb8166711B16732E2b496b',
registryContract: '0x1428e5d2356b13778A13108b10c440C83011dfB8',
echoContract: '0xcDD1fc3F5ac2782D83449d3AbE80D6b7B273B0e5',
aggregatorContract: '0x4088712AC9fad39ea133cdb9130E465d235e9642',
reverseRecordsContract: '0xEc29700C0283e5Be64AcdFe8077d6cC95dE23C23',
tornadoSubgraph: 'tornadocash/sepolia-tornado-subgraph',
@ -730,12 +711,80 @@ export const networkConfig: networkConfig = {
ENCRYPTED_NOTES_BLOCK: 5594395,
MINING_BLOCK_TIME: 15,
},
'torn.contract.tornadocash.eth': '0x3AE6667167C0f44394106E197904519D808323cA',
'governance.contract.tornadocash.eth': '0xe5324cD7602eeb387418e594B87aCADee08aeCAD',
'tornado-router.contract.tornadocash.eth': '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
},
};
export const subdomains = enabledChains.map((chain) => networkConfig[`netId${chain}`].ensSubdomainKey);
export const enabledChains = Object.values(NetId).filter((n) => typeof n === 'number') as NetIdType[];
export default networkConfig;
/**
* Custom config object to extend default config
*
* Inspired by getUrlFunc from ethers.js
* https://github.com/ethers-io/ethers.js/blob/v6/src.ts/utils/fetch.ts#L59
*/
export let customConfig: networkConfig = {};
/**
* Add or override existing network config object
*
* Could be also called on the UI hook so that the UI could allow people to use custom privacy pools
*/
export function addNetwork(newConfig: networkConfig) {
enabledChains.push(
...Object.keys(newConfig)
.map((netId) => Number(netId))
.filter((netId) => !enabledChains.includes(netId)),
);
customConfig = {
...customConfig,
...newConfig,
};
}
export function getNetworkConfig(): networkConfig {
// customConfig object
const allConfig = {
...defaultConfig,
...customConfig,
};
return enabledChains.reduce((acc, curr) => {
acc[curr] = allConfig[curr];
return acc;
}, {} as networkConfig);
}
export function getConfig(netId: NetIdType) {
const allConfig = getNetworkConfig();
const chainConfig = allConfig[netId];
if (!chainConfig) {
const errMsg = `No config found for network ${netId}!`;
throw new Error(errMsg);
}
return chainConfig;
}
export function getInstanceByAddress({ netId, address }: { netId: NetIdType; address: string }) {
const { tokens } = getConfig(netId);
for (const [currency, { instanceAddress }] of Object.entries(tokens)) {
for (const [amount, instance] of Object.entries(instanceAddress)) {
if (instance === address) {
return {
amount,
currency,
};
}
}
}
}
export function getSubdomains() {
const allConfig = getNetworkConfig();
return enabledChains.map((chain) => allConfig[chain].ensSubdomainKey);
}

View File

@ -60,3 +60,18 @@ export function parseKey(value?: string): string {
}
return value;
}
/**
* Recovery key shouldn't have a 0x prefix (Also this is how the UI generates)
*/
export function parseRecoveryKey(value?: string): string {
if (!value) {
throw new InvalidArgumentError('Invalid Recovery Key');
}
try {
computeAddress('0x' + value);
} catch {
throw new InvalidArgumentError('Invalid Recovery Key');
}
return value;
}

View File

@ -22,7 +22,6 @@ import {
Network,
parseUnits,
FetchUrlFeeDataNetworkPlugin,
BigNumberish,
FeeData,
EnsPlugin,
GasCostPlugin,
@ -30,7 +29,7 @@ import {
import type { RequestInfo, RequestInit, Response, HeadersInit } from 'node-fetch';
import { GasPriceOracle, GasPriceOracle__factory, Multicall, Multicall__factory } from '../typechain';
import { isNode, sleep } from './utils';
import type { Config } from './networkConfig';
import type { Config, NetIdType } from './networkConfig';
import { multicall } from './multicall';
declare global {
@ -213,47 +212,47 @@ export async function fetchData(url: string, options: fetchDataOptions = {}) {
throw errorObject;
}
/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-explicit-any */
export const fetchGetUrlFunc =
(options: fetchDataOptions = {}): FetchGetUrlFunc =>
async (req, _signal) => {
let signal;
async (req, _signal) => {
let signal;
if (_signal) {
const controller = new AbortController();
signal = controller.signal;
_signal.addListener(() => {
controller.abort();
});
}
const init = {
...options,
method: req.method || 'POST',
headers: req.headers,
body: req.body || undefined,
signal,
returnResponse: true,
};
const resp = await fetchData(req.url, init);
const headers = {} as { [key in string]: any };
resp.headers.forEach((value: any, key: string) => {
headers[key.toLowerCase()] = value;
if (_signal) {
const controller = new AbortController();
signal = controller.signal;
_signal.addListener(() => {
controller.abort();
});
}
const respBody = await resp.arrayBuffer();
const body = respBody == null ? null : new Uint8Array(respBody);
return {
statusCode: resp.status,
statusMessage: resp.statusText,
headers,
body,
};
const init = {
...options,
method: req.method || 'POST',
headers: req.headers,
body: req.body || undefined,
signal,
returnResponse: true,
};
/* eslint-enable prettier/prettier, @typescript-eslint/no-explicit-any */
const resp = await fetchData(req.url, init);
const headers = {} as { [key in string]: any };
resp.headers.forEach((value: any, key: string) => {
headers[key.toLowerCase()] = value;
});
const respBody = await resp.arrayBuffer();
const body = respBody == null ? null : new Uint8Array(respBody);
return {
statusCode: resp.status,
statusMessage: resp.statusText,
headers,
body,
};
};
/* eslint-enable @typescript-eslint/no-explicit-any */
// caching to improve performance
const oracleMapper = new Map();
@ -366,7 +365,7 @@ export async function getProvider(rpcUrl: string, fetchOptions?: getProviderOpti
}
export function getProviderWithNetId(
netId: BigNumberish,
netId: NetIdType,
rpcUrl: string,
config: Config,
fetchOptions?: getProviderOptions,
@ -604,7 +603,7 @@ export type handleWalletFunc = (...args: any[]) => void;
/* eslint-enable @typescript-eslint/no-explicit-any */
export type TornadoBrowserProviderOptions = TornadoWalletOptions & {
webChainId?: BigNumberish;
webChainId?: NetIdType;
connectWallet?: connectWalletFunc;
handleNetworkChanges?: handleWalletFunc;
handleAccountChanges?: handleWalletFunc;

View File

@ -1,8 +1,8 @@
import { namehash, parseEther } from 'ethers';
import { getAddress, namehash, parseEther } from 'ethers';
import type { Aggregator } from '@tornado/contracts';
import type { RelayerStructOutput } from '@tornado/contracts/dist/contracts/Governance/Aggregator/Aggregator';
import { sleep } from './utils';
import type { Config } from './networkConfig';
import { NetId, NetIdType, Config } from './networkConfig';
import { fetchData, fetchDataOptions } from './providers';
import { ajv, jobsSchema, getStatusSchema } from './schemas';
import type { snarkProofs } from './websnark';
@ -15,21 +15,23 @@ export interface RelayerParams {
}
export interface Relayer {
netId: number;
netId: NetIdType;
url: string;
hostname: string;
rewardAccount: string;
instances: string[];
gasPrice?: number;
ethPrices?: {
[key in string]: string;
};
currentQueue: number;
tornadoServiceFee: number;
}
export type RelayerInfo = Relayer & {
hostname: string;
ensName: string;
stakeBalance: bigint;
relayerAddress: string;
ethPrices?: {
[key in string]: string;
};
};
export type RelayerError = {
@ -55,7 +57,7 @@ export interface RelayerStatus {
fast: number;
additionalProperties?: number;
};
netId: number;
netId: NetIdType;
ethPrices?: {
[key in string]: string;
};
@ -105,14 +107,14 @@ export function parseSemanticVersion(version: string) {
return groups as unknown as semanticVersion;
}
export function isRelayerUpdated(relayerVersion: string, netId: number | string) {
export function isRelayerUpdated(relayerVersion: string, netId: NetIdType) {
const { major, patch, prerelease } = parseSemanticVersion(relayerVersion);
// Save backwards compatibility with V4 relayers for Ethereum Mainnet
const requiredMajor = netId === 1 ? '4' : '5';
const requiredMajor = netId === NetId.MAINNET ? '4' : '5';
const isUpdatedMajor = major === requiredMajor;
if (prerelease) return false;
return isUpdatedMajor && (Number(patch) >= 5 || Number(netId) !== 1); // Patch checking - also backwards compatibility for Mainnet
return isUpdatedMajor && (Number(patch) >= 5 || netId !== NetId.MAINNET); // Patch checking - also backwards compatibility for Mainnet
}
export function calculateScore({ stakeBalance, tornadoServiceFee }: RelayerInfo, minFee = 0.33, maxFee = 0.53) {
@ -139,10 +141,28 @@ export function getWeightRandom(weightsScores: bigint[], random: bigint) {
return Math.floor(Math.random() * weightsScores.length);
}
export function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: string | number) {
export type RelayerInstanceList = {
[key in string]: {
instanceAddress: {
[key in string]: string;
};
};
};
export function getSupportedInstances(instanceList: RelayerInstanceList) {
const rawList = Object.values(instanceList)
.map(({ instanceAddress }) => {
return Object.values(instanceAddress);
})
.flat();
return rawList.map((l) => getAddress(l));
}
export function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: NetIdType) {
let minFee: number, maxFee: number;
if (Number(netId) !== 1) {
if (netId !== NetId.MAINNET) {
minFee = 0.01;
maxFee = 0.3;
}
@ -159,7 +179,7 @@ export function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: string
}
export interface RelayerClientConstructor {
netId: number | string;
netId: NetIdType;
config: Config;
Aggregator: Aggregator;
fetchDataOptions?: fetchDataOptions;
@ -170,14 +190,14 @@ export type RelayerClientWithdraw = snarkProofs & {
};
export class RelayerClient {
netId: number;
netId: NetIdType;
config: Config;
Aggregator: Aggregator;
selectedRelayer?: Relayer;
fetchDataOptions?: fetchDataOptions;
constructor({ netId, config, Aggregator, fetchDataOptions }: RelayerClientConstructor) {
this.netId = Number(netId);
this.netId = netId;
this.config = config;
this.Aggregator = Aggregator;
this.fetchDataOptions = fetchDataOptions;
@ -220,7 +240,7 @@ export class RelayerClient {
throw new Error('This relayer serves a different network');
}
if (relayerAddress && this.netId === 1 && status.rewardAccount !== relayerAddress) {
if (relayerAddress && this.netId === NetId.MAINNET && status.rewardAccount !== relayerAddress) {
throw new Error('The Relayer reward address must match registered address');
}
@ -263,7 +283,9 @@ export class RelayerClient {
ensName,
stakeBalance,
relayerAddress,
rewardAccount: status.rewardAccount,
rewardAccount: getAddress(status.rewardAccount),
instances: getSupportedInstances(status.instances),
gasPrice: status.gasPrices?.fast,
ethPrices: status.ethPrices,
currentQueue: status.currentQueue,
tornadoServiceFee: status.tornadoServiceFee,

View File

@ -1,4 +1,4 @@
import type { Config } from '../networkConfig';
import { Config, NetId, NetIdType } from '../networkConfig';
export type statusInstanceType = {
type: string;
@ -110,7 +110,7 @@ const statusSchema: statusSchema = {
required: ['rewardAccount', 'instances', 'netId', 'tornadoServiceFee', 'version', 'health'],
};
export function getStatusSchema(netId: number | string, config: Config) {
export function getStatusSchema(netId: NetIdType, config: Config) {
const { tokens, optionalTokens = [], nativeCurrency } = config;
// deep copy schema
@ -162,7 +162,7 @@ export function getStatusSchema(netId: number | string, config: Config) {
schema.properties.instances = instances;
if (Number(netId) === 1) {
if (netId === NetId.MAINNET) {
const _tokens = Object.keys(tokens).filter((t) => t !== nativeCurrency);
const ethPrices: statusEthPricesType = {

113
src/services/treeCache.ts Normal file
View File

@ -0,0 +1,113 @@
/**
* Create tree cache file from node.js
*
* Only works for node.js, modified from https://github.com/tornadocash/tornado-classic-ui/blob/master/scripts/updateTree.js
*/
import { MerkleTree } from '@tornado/fixed-merkle-tree';
import BloomFilter from 'bloomfilter.js';
import { saveUserFile } from './data';
import { DepositsEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface TreeCacheConstructor {
netId: NetIdType;
amount: string;
currency: string;
userDirectory: string;
PARTS_COUNT?: number;
LEAVES?: number;
zeroElement?: string;
}
export interface treeMetadata {
blockNumber: number;
logIndex: number;
transactionHash: string;
timestamp: number;
from: string;
leafIndex: number;
}
export class TreeCache {
netId: NetIdType;
amount: string;
currency: string;
userDirectory: string;
PARTS_COUNT: number;
constructor({ netId, amount, currency, userDirectory, PARTS_COUNT = 4 }: TreeCacheConstructor) {
this.netId = netId;
this.amount = amount;
this.currency = currency;
this.userDirectory = userDirectory;
this.PARTS_COUNT = PARTS_COUNT;
}
getInstanceName(): string {
return `deposits_${this.netId}_${this.currency}_${this.amount}`;
}
async createTree(events: DepositsEvents[], tree: MerkleTree) {
const bloom = new BloomFilter(events.length);
console.log(`Creating cached tree for ${this.getInstanceName()}\n`);
// events indexed by commitment
const eventsData = events.reduce(
(acc, { leafIndex, commitment, ...rest }, i) => {
if (leafIndex !== i) {
throw new Error(`leafIndex (${leafIndex}) !== i (${i})`);
}
acc[commitment] = { ...rest, leafIndex };
return acc;
},
{} as { [key in string]: treeMetadata },
);
const slices = tree.getTreeSlices(this.PARTS_COUNT);
await Promise.all(
slices.map(async (slice, index) => {
const metadata = slice.elements.reduce((acc, curr) => {
if (index < this.PARTS_COUNT - 1) {
bloom.add(curr);
}
acc.push(eventsData[curr]);
return acc;
}, [] as treeMetadata[]);
const dataString =
JSON.stringify(
{
...slice,
metadata,
},
null,
2,
) + '\n';
const fileName = `${this.getInstanceName()}_slice${index + 1}.json`;
await saveUserFile({
fileName,
userDirectory: this.userDirectory,
dataString,
});
}),
);
const dataString = bloom.serialize() + '\n';
const fileName = `${this.getInstanceName()}_bloom.json`;
await saveUserFile({
fileName,
userDirectory: this.userDirectory,
dataString,
});
}
}

View File

@ -1,4 +1,4 @@
import { URL } from 'url';
import { webcrypto } from 'crypto';
import BN from 'bn.js';
import type { BigNumberish } from 'ethers';
@ -16,6 +16,8 @@ export const isNode =
}
).browser && typeof globalThis.window === 'undefined';
export const crypto = isNode ? webcrypto : (globalThis.crypto as typeof webcrypto);
export const chunk = <T>(arr: T[], size: number): T[][] =>
[...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(size * i, size + size * i));
@ -35,26 +37,28 @@ export function validateUrl(url: string, protocols?: string[]) {
}
}
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
const totalSize = arrays.reduce((acc, e) => acc + e.length, 0);
const merged = new Uint8Array(totalSize);
arrays.forEach((array, i, arrays) => {
const offset = arrays.slice(0, i).reduce((acc, e) => acc + e.length, 0);
merged.set(array, offset);
});
return merged;
}
export function bufferToBytes(b: Buffer) {
return new Uint8Array(b.buffer);
}
export function bytesToBase64(bytes: Uint8Array) {
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; ++i) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
return btoa(String.fromCharCode.apply(null, Array.from(bytes)));
}
export function base64ToBytes(base64: string) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
export function bytesToHex(bytes: Uint8Array) {
@ -66,6 +70,16 @@ export function bytesToHex(bytes: Uint8Array) {
);
}
export function hexToBytes(hexString: string) {
if (hexString.slice(0, 2) === '0x') {
hexString = hexString.replace('0x', '');
}
if (hexString.length % 2 !== 0) {
hexString = '0' + hexString;
}
return Uint8Array.from((hexString.match(/.{1,2}/g) as string[]).map((byte) => parseInt(byte, 16)));
}
// Convert BE encoded bytes (Buffer | Uint8Array) array to BigInt
export function bytesToBN(bytes: Uint8Array) {
return BigInt(bytesToHex(bytes));

25
src/types/bloomfilter.js.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'bloomfilter.js' {
export default class BloomFilter {
m: number;
k: number;
size: number;
bitview: any;
constructor(n: number, false_postive_tolerance?: number);
calculateHash(x: number, m: number, i: number): number;
test(data: any): boolean;
add(data: any): void;
bytelength(): number;
view(): Uint8Array;
serialize(): string;
deserialize(serialized: string): BloomFilter;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -19824,5 +19824,5 @@ if (isNode && threads) {
postMessage(merkleTree.toString());
}));
} else {
throw new Error("This browser / environment doesn't support workers!");
throw new Error("This browser / environment does not support workers!");
}

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,7 @@
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./src/types",
],
/* Visit https://aka.ms/tsconfig to read more about this file */

100
webpack.config.js Normal file
View File

@ -0,0 +1,100 @@
const esbuild = require('esbuild');
const path = require('path');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const esbuildLoader = {
test: /\.ts?$/,
loader: 'esbuild-loader',
options: {
loader: 'ts',
target: 'es2016',
implementation: esbuild
}
}
const commonAlias = {
fs: false,
'fs/promises': false,
'path': false,
'url': false,
'worker_threads': false,
'fflate': 'fflate/browser',
'@colors/colors': false,
'cli-table3': false,
'commander': false,
'http-proxy-agent': false,
'https-proxy-agent': false,
'socks-proxy-agent': false,
}
module.exports = [
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/index.ts',
output: {
filename: 'index.umd.js',
path: path.resolve(__dirname, './dist'),
library: 'Tornado',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
},
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/cli.ts',
output: {
filename: 'cli.js',
path: path.resolve(__dirname, './dist'),
},
target: 'node',
plugins: [],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {}
},
optimization: {
minimize: false,
}
},
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/merkleTreeWorker.ts',
output: {
filename: 'merkleTreeWorker.umd.js',
path: path.resolve(__dirname, './static'),
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
}
];

1403
yarn.lock

File diff suppressed because it is too large Load Diff