Compare commits

...

2 Commits

Author SHA1 Message Date
8041bd7f78 Remove unused constructor params 2024-10-26 19:20:31 +00:00
acb7aa72a1 Use 4 tab size 2024-10-26 19:07:06 +00:00
58 changed files with 11152 additions and 8293 deletions

View File

@@ -1,66 +1,43 @@
module.exports = { module.exports = {
"env": { env: {
"es2021": true, es2021: true,
"node": true node: true,
}, },
"extends": [ extends: [
"eslint:recommended", 'prettier',
"plugin:@typescript-eslint/recommended", 'eslint:recommended',
"plugin:import/recommended", 'plugin:@typescript-eslint/recommended',
"plugin:import/typescript", 'plugin:import/recommended',
"prettier", 'plugin:import/typescript',
"plugin:prettier/recommended", 'plugin:prettier/recommended',
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
singleQuote: true,
printWidth: 120
}
], ],
"import/order": ["error"], overrides: [
/** {
"indent": [ env: {
"error", node: true,
2 },
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script',
},
},
], ],
**/ parser: '@typescript-eslint/parser',
"linebreak-style": [ parserOptions: {
"error", ecmaVersion: 'latest',
"unix" sourceType: 'module',
], },
"quotes": [ plugins: ['@typescript-eslint', 'prettier'],
"error", rules: {
"single" 'prettier/prettier': [
], 'error',
"semi": [ {
"error", tabWidth: 4,
"always" singleQuote: true,
], },
"@typescript-eslint/no-unused-vars": ["warn"], ],
"@typescript-eslint/no-unused-expressions": ["off"] 'import/order': ['error'],
} '@typescript-eslint/no-unused-vars': ['warn'],
} '@typescript-eslint/no-unused-expressions': ['off'],
},
};

22
.nycrc
View File

@@ -1,13 +1,13 @@
{ {
"all": true, "all": true,
"extension": [".ts"], "extension": [".ts"],
"report-dir": "coverage", "report-dir": "coverage",
"reporter": [ "reporter": [
"html", "html",
"lcov", "lcov",
"text", "text",
"text-summary" "text-summary"
], ],
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["src/typechain/**/*"] "exclude": ["src/typechain/**/*"]
} }

4
dist/batch.d.ts vendored
View File

@@ -80,7 +80,7 @@ export declare class BatchEventsService {
retryMax: number; retryMax: number;
retryOn: number; retryOn: number;
constructor({ provider, contract, onProgress, concurrencySize, blocksPerRequest, shouldRetry, retryMax, retryOn, }: BatchEventServiceConstructor); constructor({ provider, contract, onProgress, concurrencySize, blocksPerRequest, shouldRetry, retryMax, retryOn, }: BatchEventServiceConstructor);
getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]>; getPastEvents({ fromBlock, toBlock, type, }: EventInput): Promise<EventLog[]>;
createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[]; createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[];
getBatchEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]>; getBatchEvents({ fromBlock, toBlock, type, }: EventInput): Promise<EventLog[]>;
} }

4
dist/deposits.d.ts vendored
View File

@@ -30,7 +30,7 @@ export interface parsedInvoiceExec extends DepositType {
} }
export declare function parseNote(noteString: string): parsedNoteExec | undefined; export declare function parseNote(noteString: string): parsedNoteExec | undefined;
export declare function parseInvoice(invoiceString: string): parsedInvoiceExec | undefined; export declare function parseInvoice(invoiceString: string): parsedInvoiceExec | undefined;
export declare function createDeposit({ nullifier, secret }: createDepositParams): Promise<createDepositObject>; export declare function createDeposit({ nullifier, secret, }: createDepositParams): Promise<createDepositObject>;
export interface DepositConstructor { export interface DepositConstructor {
currency: string; currency: string;
amount: string; amount: string;
@@ -56,7 +56,7 @@ export declare class Deposit {
nullifierHex: string; nullifierHex: string;
constructor({ currency, amount, netId, nullifier, secret, note, noteHex, invoice, commitmentHex, nullifierHex, }: DepositConstructor); constructor({ currency, amount, netId, nullifier, secret, note, noteHex, invoice, commitmentHex, nullifierHex, }: DepositConstructor);
toString(): string; toString(): string;
static createNote({ currency, amount, netId, nullifier, secret }: createNoteParams): Promise<Deposit>; static createNote({ currency, amount, netId, nullifier, secret, }: createNoteParams): Promise<Deposit>;
static parseNote(noteString: string): Promise<Deposit>; static parseNote(noteString: string): Promise<Deposit>;
} }
export declare class Invoice { export declare class Invoice {

View File

@@ -1,7 +1,6 @@
import { EthEncryptedData } from '@metamask/eth-sig-util'; import { EthEncryptedData } from '@metamask/eth-sig-util';
import { Signer, Wallet } from 'ethers'; import { Signer, Wallet } from 'ethers';
import { EchoEvents, EncryptedNotesEvents } from './events'; import { EchoEvents, EncryptedNotesEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface NoteToEncrypt { export interface NoteToEncrypt {
address: string; address: string;
noteHex: string; noteHex: string;
@@ -11,22 +10,20 @@ export interface DecryptedNotes {
address: string; address: string;
noteHex: string; noteHex: string;
} }
export declare function packEncryptedMessage({ nonce, ephemPublicKey, ciphertext }: EthEncryptedData): string; export declare function packEncryptedMessage({ nonce, ephemPublicKey, ciphertext, }: EthEncryptedData): string;
export declare function unpackEncryptedMessage(encryptedMessage: string): EthEncryptedData & { export declare function unpackEncryptedMessage(encryptedMessage: string): EthEncryptedData & {
messageBuff: string; messageBuff: string;
}; };
export interface NoteAccountConstructor { export interface NoteAccountConstructor {
netId: NetIdType;
blockNumber?: number; blockNumber?: number;
recoveryKey?: string; recoveryKey?: string;
} }
export declare class NoteAccount { export declare class NoteAccount {
netId: NetIdType;
blockNumber?: number; blockNumber?: number;
recoveryKey: string; recoveryKey: string;
recoveryAddress: string; recoveryAddress: string;
recoveryPublicKey: string; recoveryPublicKey: string;
constructor({ netId, blockNumber, recoveryKey }: NoteAccountConstructor); constructor({ blockNumber, recoveryKey }: NoteAccountConstructor);
/** /**
* Intends to mock eth_getEncryptionPublicKey behavior from MetaMask * Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key * In order to make the recoveryKey retrival from Echoer possible from the bare private key
@@ -39,7 +36,7 @@ export declare class NoteAccount {
/** /**
* Decrypt Echoer backuped note encryption account with private keys * Decrypt Echoer backuped note encryption account with private keys
*/ */
decryptSignerNoteAccounts(signer: Signer | Wallet, events: EchoEvents[]): Promise<NoteAccount[]>; static decryptSignerNoteAccounts(signer: Signer | Wallet, events: EchoEvents[]): Promise<NoteAccount[]>;
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[]; decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[];
encryptNote({ address, noteHex }: NoteToEncrypt): string; encryptNote({ address, noteHex }: NoteToEncrypt): string;
} }

16
dist/events/base.d.ts vendored
View File

@@ -50,10 +50,10 @@ export declare class BaseEventsService<EventType extends MinimalEvents> {
getTovarishType(): string; getTovarishType(): string;
getGraphMethod(): string; getGraphMethod(): string;
getGraphParams(): BaseGraphParams; getGraphParams(): BaseGraphParams;
updateEventProgress({ percentage, type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]): void; updateEventProgress({ percentage, type, fromBlock, toBlock, count, }: Parameters<BatchEventOnProgress>[0]): void;
updateBlockProgress({ percentage, currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]): void; updateBlockProgress({ percentage, currentIndex, totalIndex, }: Parameters<BatchBlockOnProgress>[0]): void;
updateTransactionProgress({ percentage, currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]): void; updateTransactionProgress({ percentage, currentIndex, totalIndex, }: Parameters<BatchBlockOnProgress>[0]): void;
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchGraphOnProgress>[0]): void; updateGraphProgress({ type, fromBlock, toBlock, count, }: Parameters<BatchGraphOnProgress>[0]): void;
formatEvents(events: EventLog[]): Promise<EventType[]>; formatEvents(events: EventLog[]): Promise<EventType[]>;
/** /**
* Get saved or cached events * Get saved or cached events
@@ -75,7 +75,7 @@ export declare class BaseEventsService<EventType extends MinimalEvents> {
fromBlock: number; fromBlock: number;
toBlock?: number; toBlock?: number;
}): Promise<BaseEvents<EventType>>; }): Promise<BaseEvents<EventType>>;
getLatestEvents({ fromBlock }: { getLatestEvents({ fromBlock, }: {
fromBlock: number; fromBlock: number;
}): Promise<BaseEvents<EventType>>; }): Promise<BaseEvents<EventType>>;
validateEvents<S>({ events, lastBlock, hasNewEvents, }: BaseEvents<EventType> & { validateEvents<S>({ events, lastBlock, hasNewEvents, }: BaseEvents<EventType> & {
@@ -120,7 +120,7 @@ export declare class BaseTornadoService extends BaseEventsService<DepositsEvents
validateEvents<S>({ events, hasNewEvents, }: BaseEvents<DepositsEvents | WithdrawalsEvents> & { validateEvents<S>({ events, hasNewEvents, }: BaseEvents<DepositsEvents | WithdrawalsEvents> & {
hasNewEvents?: boolean; hasNewEvents?: boolean;
}): Promise<S>; }): Promise<S>;
getLatestEvents({ fromBlock }: { getLatestEvents({ fromBlock, }: {
fromBlock: number; fromBlock: number;
}): Promise<BaseEvents<DepositsEvents | WithdrawalsEvents>>; }): Promise<BaseEvents<DepositsEvents | WithdrawalsEvents>>;
} }
@@ -132,7 +132,7 @@ export declare class BaseEchoService extends BaseEventsService<EchoEvents> {
getInstanceName(): string; getInstanceName(): string;
getGraphMethod(): string; getGraphMethod(): string;
formatEvents(events: EventLog[]): Promise<EchoEvents[]>; formatEvents(events: EventLog[]): Promise<EchoEvents[]>;
getEventsFromGraph({ fromBlock }: { getEventsFromGraph({ fromBlock, }: {
fromBlock: number; fromBlock: number;
}): Promise<BaseEvents<EchoEvents>>; }): Promise<BaseEvents<EchoEvents>>;
} }
@@ -180,7 +180,7 @@ export declare class BaseGovernanceService extends BaseEventsService<AllGovernan
getTovarishType(): string; getTovarishType(): string;
getGraphMethod(): string; getGraphMethod(): string;
formatEvents(events: EventLog[]): Promise<AllGovernanceEvents[]>; formatEvents(events: EventLog[]): Promise<AllGovernanceEvents[]>;
getEventsFromGraph({ fromBlock }: { getEventsFromGraph({ fromBlock, }: {
fromBlock: number; fromBlock: number;
}): Promise<BaseEvents<AllGovernanceEvents>>; }): Promise<BaseEvents<AllGovernanceEvents>>;
getAllProposals(): Promise<GovernanceProposals[]>; getAllProposals(): Promise<GovernanceProposals[]>;

2
dist/events/db.d.ts vendored
View File

@@ -28,7 +28,7 @@ export declare class DBTornadoService extends BaseTornadoService {
constructor(params: DBTornadoServiceConstructor); constructor(params: DBTornadoServiceConstructor);
getEventsFromDB(): Promise<BaseEvents<DepositsEvents | WithdrawalsEvents>>; getEventsFromDB(): Promise<BaseEvents<DepositsEvents | WithdrawalsEvents>>;
getEventsFromCache(): Promise<CachedEvents<DepositsEvents | WithdrawalsEvents>>; getEventsFromCache(): Promise<CachedEvents<DepositsEvents | WithdrawalsEvents>>;
saveEvents({ events, lastBlock }: BaseEvents<DepositsEvents | WithdrawalsEvents>): Promise<void>; saveEvents({ events, lastBlock, }: BaseEvents<DepositsEvents | WithdrawalsEvents>): Promise<void>;
} }
export interface DBEchoServiceConstructor extends BaseEchoServiceConstructor { export interface DBEchoServiceConstructor extends BaseEchoServiceConstructor {
staticUrl: string; staticUrl: string;

View File

@@ -57,7 +57,7 @@ export interface getMetaReturns {
lastSyncBlock: null | number; lastSyncBlock: null | number;
hasIndexingErrors: null | boolean; hasIndexingErrors: null | boolean;
} }
export declare function getMeta({ graphApi, subgraphName, fetchDataOptions }: getMetaParams): Promise<getMetaReturns>; export declare function getMeta({ graphApi, subgraphName, fetchDataOptions, }: getMetaParams): Promise<getMetaReturns>;
export interface GraphRegisters { export interface GraphRegisters {
relayers: { relayers: {
id: string; id: string;

8
dist/idb.d.ts vendored
View File

@@ -35,16 +35,16 @@ export declare class IndexedDB {
key?: string; key?: string;
count?: number; count?: number;
}): Promise<T>; }): Promise<T>;
getItem<T>({ storeName, key }: { getItem<T>({ storeName, key, }: {
storeName: string; storeName: string;
key: string; key: string;
}): Promise<T | undefined>; }): Promise<T | undefined>;
addItem({ storeName, data, key }: { addItem({ storeName, data, key, }: {
storeName: string; storeName: string;
data: any; data: any;
key: string; key: string;
}): Promise<void>; }): Promise<void>;
putItem({ storeName, data, key }: { putItem({ storeName, data, key, }: {
storeName: string; storeName: string;
data: any; data: any;
key?: string; key?: string;
@@ -62,7 +62,7 @@ export declare class IndexedDB {
getValue<T>(key: string): Promise<T | undefined>; getValue<T>(key: string): Promise<T | undefined>;
setValue(key: string, data: any): Promise<void>; setValue(key: string, data: any): Promise<void>;
delValue(key: string): Promise<void>; delValue(key: string): Promise<void>;
clearStore({ storeName, mode }: { clearStore({ storeName, mode, }: {
storeName: string; storeName: string;
mode: IDBTransactionMode; mode: IDBTransactionMode;
}): Promise<void>; }): Promise<void>;

1484
dist/index.js vendored

File diff suppressed because it is too large Load Diff

1458
dist/index.mjs vendored

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ export declare class MerkleTreeService {
merkleWorkerPath?: string; merkleWorkerPath?: string;
constructor({ netId, amount, currency, Tornado, commitmentHex, merkleTreeHeight, emptyElement, merkleWorkerPath, }: MerkleTreeConstructor); constructor({ netId, amount, currency, Tornado, commitmentHex, merkleTreeHeight, emptyElement, merkleWorkerPath, }: MerkleTreeConstructor);
createTree(events: Element[]): Promise<MerkleTree>; createTree(events: Element[]): Promise<MerkleTree>;
createPartialTree({ edge, elements }: { createPartialTree({ edge, elements, }: {
edge: TreeEdge; edge: TreeEdge;
elements: Element[]; elements: Element[];
}): Promise<PartialMerkleTree>; }): Promise<PartialMerkleTree>;

View File

@@ -1814,7 +1814,9 @@ class Mimc {
} }
async initMimc() { async initMimc() {
this.sponge = await buildMimcSponge(); this.sponge = await buildMimcSponge();
this.hash = (left, right) => this.sponge?.F.toString(this.sponge?.multiHash([BigInt(left), BigInt(right)])); this.hash = (left, right) => this.sponge?.F.toString(
this.sponge?.multiHash([BigInt(left), BigInt(right)])
);
} }
async getHash() { async getHash() {
await this.mimcPromise; await this.mimcPromise;
@@ -1835,18 +1837,27 @@ async function nodePostWork() {
const { hash: hashFunction } = await mimc.getHash(); const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = workerThreads.workerData; const { merkleTreeHeight, edge, elements, zeroElement } = workerThreads.workerData;
if (edge) { if (edge) {
const merkleTree2 = new libExports.PartialMerkleTree(merkleTreeHeight, edge, elements, { const merkleTree2 = new libExports.PartialMerkleTree(
zeroElement, merkleTreeHeight,
hashFunction edge,
}); elements,
workerThreads.parentPort.postMessage(merkleTree2.toString()); {
zeroElement,
hashFunction
}
);
workerThreads.parentPort.postMessage(
merkleTree2.toString()
);
return; return;
} }
const merkleTree = new libExports.MerkleTree(merkleTreeHeight, elements, { const merkleTree = new libExports.MerkleTree(merkleTreeHeight, elements, {
zeroElement, zeroElement,
hashFunction hashFunction
}); });
workerThreads.parentPort.postMessage(merkleTree.toString()); workerThreads.parentPort.postMessage(
merkleTree.toString()
);
} }
if (isNode && workerThreads) { if (isNode && workerThreads) {
nodePostWork(); nodePostWork();
@@ -1861,10 +1872,15 @@ if (isNode && workerThreads) {
const { hash: hashFunction } = await mimc.getHash(); const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = data; const { merkleTreeHeight, edge, elements, zeroElement } = data;
if (edge) { if (edge) {
const merkleTree2 = new libExports.PartialMerkleTree(merkleTreeHeight, edge, elements, { const merkleTree2 = new libExports.PartialMerkleTree(
zeroElement, merkleTreeHeight,
hashFunction edge,
}); elements,
{
zeroElement,
hashFunction
}
);
postMessage(merkleTree2.toString()); postMessage(merkleTree2.toString());
return; return;
} }

View File

@@ -101986,7 +101986,9 @@ class Mimc {
} }
async initMimc() { async initMimc() {
this.sponge = await mimcsponge_buildMimcSponge(); this.sponge = await mimcsponge_buildMimcSponge();
this.hash = (left, right) => this.sponge?.F.toString(this.sponge?.multiHash([BigInt(left), BigInt(right)])); this.hash = (left, right) => this.sponge?.F.toString(
this.sponge?.multiHash([BigInt(left), BigInt(right)])
);
} }
async getHash() { async getHash() {
await this.mimcPromise; await this.mimcPromise;
@@ -102011,7 +102013,9 @@ BigInt.prototype.toJSON = function() {
}; };
const isNode = !process.browser && typeof globalThis.window === "undefined"; const isNode = !process.browser && typeof globalThis.window === "undefined";
const utils_crypto = isNode ? crypto_browserify.webcrypto : globalThis.crypto; const utils_crypto = isNode ? crypto_browserify.webcrypto : globalThis.crypto;
const chunk = (arr, size) => [...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(size * i, size + size * i)); const chunk = (arr, size) => [...Array(Math.ceil(arr.length / size))].map(
(_, i) => arr.slice(size * i, size + size * i)
);
function utils_sleep(ms) { function utils_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
@@ -102039,7 +102043,9 @@ function bufferToBytes(b) {
return new Uint8Array(b.buffer); return new Uint8Array(b.buffer);
} }
function bytesToBase64(bytes) { function bytesToBase64(bytes) {
return btoa(bytes.reduce((data, byte) => data + String.fromCharCode(byte), "")); return btoa(
bytes.reduce((data, byte) => data + String.fromCharCode(byte), "")
);
} }
function base64ToBytes(base64) { function base64ToBytes(base64) {
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
@@ -102054,7 +102060,11 @@ function hexToBytes(hexString) {
if (hexString.length % 2 !== 0) { if (hexString.length % 2 !== 0) {
hexString = "0" + hexString; hexString = "0" + hexString;
} }
return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); return Uint8Array.from(
hexString.match(/.{1,2}/g).map(
(byte) => parseInt(byte, 16)
)
);
} }
function bytesToBN(bytes) { function bytesToBN(bytes) {
return BigInt(bytesToHex(bytes)); return BigInt(bytesToHex(bytes));
@@ -102067,7 +102077,11 @@ function bnToBytes(bigint) {
if (hexString.length % 2 !== 0) { if (hexString.length % 2 !== 0) {
hexString = "0" + hexString; hexString = "0" + hexString;
} }
return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); return Uint8Array.from(
hexString.match(/.{1,2}/g).map(
(byte) => parseInt(byte, 16)
)
);
} }
function leBuff2Int(bytes) { function leBuff2Int(bytes) {
return new BN(bytes, 16, "le"); return new BN(bytes, 16, "le");
@@ -102128,18 +102142,27 @@ async function nodePostWork() {
const { hash: hashFunction } = await mimc.getHash(); const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = (worker_threads_ignored_default()).workerData; const { merkleTreeHeight, edge, elements, zeroElement } = (worker_threads_ignored_default()).workerData;
if (edge) { if (edge) {
const merkleTree2 = new lib.PartialMerkleTree(merkleTreeHeight, edge, elements, { const merkleTree2 = new lib.PartialMerkleTree(
zeroElement, merkleTreeHeight,
hashFunction edge,
}); elements,
worker_threads_ignored_default().parentPort.postMessage(merkleTree2.toString()); {
zeroElement,
hashFunction
}
);
worker_threads_ignored_default().parentPort.postMessage(
merkleTree2.toString()
);
return; return;
} }
const merkleTree = new lib.MerkleTree(merkleTreeHeight, elements, { const merkleTree = new lib.MerkleTree(merkleTreeHeight, elements, {
zeroElement, zeroElement,
hashFunction hashFunction
}); });
worker_threads_ignored_default().parentPort.postMessage(merkleTree.toString()); worker_threads_ignored_default().parentPort.postMessage(
merkleTree.toString()
);
} }
if (isNode && (worker_threads_ignored_default())) { if (isNode && (worker_threads_ignored_default())) {
nodePostWork(); nodePostWork();
@@ -102154,10 +102177,15 @@ if (isNode && (worker_threads_ignored_default())) {
const { hash: hashFunction } = await mimc.getHash(); const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = data; const { merkleTreeHeight, edge, elements, zeroElement } = data;
if (edge) { if (edge) {
const merkleTree2 = new lib.PartialMerkleTree(merkleTreeHeight, edge, elements, { const merkleTree2 = new lib.PartialMerkleTree(
zeroElement, merkleTreeHeight,
hashFunction edge,
}); elements,
{
zeroElement,
hashFunction
}
);
postMessage(merkleTree2.toString()); postMessage(merkleTree2.toString());
return; return;
} }

6
dist/providers.d.ts vendored
View File

@@ -50,7 +50,7 @@ export declare class TornadoWallet extends Wallet {
gasLimitBump: number; gasLimitBump: number;
gasFailover: boolean; gasFailover: boolean;
bumpNonce: boolean; bumpNonce: boolean;
constructor(key: string | SigningKey, provider?: Provider, { gasPriceBump, gasLimitBump, gasFailover, bumpNonce }?: TornadoWalletOptions); constructor(key: string | SigningKey, provider?: Provider, { gasPriceBump, gasLimitBump, gasFailover, bumpNonce, }?: TornadoWalletOptions);
static fromMnemonic(mneomnic: string, provider: Provider, index?: number, options?: TornadoWalletOptions): TornadoWallet; static fromMnemonic(mneomnic: string, provider: Provider, index?: number, options?: TornadoWalletOptions): TornadoWallet;
populateTransaction(tx: TransactionRequest): Promise<import("ethers").TransactionLike<string>>; populateTransaction(tx: TransactionRequest): Promise<import("ethers").TransactionLike<string>>;
} }
@@ -60,7 +60,7 @@ export declare class TornadoVoidSigner extends VoidSigner {
gasLimitBump: number; gasLimitBump: number;
gasFailover: boolean; gasFailover: boolean;
bumpNonce: boolean; bumpNonce: boolean;
constructor(address: string, provider?: Provider, { gasPriceBump, gasLimitBump, gasFailover, bumpNonce }?: TornadoWalletOptions); constructor(address: string, provider?: Provider, { gasPriceBump, gasLimitBump, gasFailover, bumpNonce, }?: TornadoWalletOptions);
populateTransaction(tx: TransactionRequest): Promise<import("ethers").TransactionLike<string>>; populateTransaction(tx: TransactionRequest): Promise<import("ethers").TransactionLike<string>>;
} }
export declare class TornadoRpcSigner extends JsonRpcSigner { export declare class TornadoRpcSigner extends JsonRpcSigner {
@@ -69,7 +69,7 @@ export declare class TornadoRpcSigner extends JsonRpcSigner {
gasLimitBump: number; gasLimitBump: number;
gasFailover: boolean; gasFailover: boolean;
bumpNonce: boolean; bumpNonce: boolean;
constructor(provider: JsonRpcApiProvider, address: string, { gasPriceBump, gasLimitBump, gasFailover, bumpNonce }?: TornadoWalletOptions); constructor(provider: JsonRpcApiProvider, address: string, { gasPriceBump, gasLimitBump, gasFailover, bumpNonce, }?: TornadoWalletOptions);
sendUncheckedTransaction(tx: TransactionRequest): Promise<string>; sendUncheckedTransaction(tx: TransactionRequest): Promise<string>;
} }
export type connectWalletFunc = (...args: any[]) => Promise<void>; export type connectWalletFunc = (...args: any[]) => Promise<void>;

View File

@@ -109,7 +109,7 @@ export function isRelayerUpdated(relayerVersion: string, netId: NetIdType) {
return isUpdatedMajor && (Number(patch) >= 5 || netId !== NetId.MAINNET); // Patch checking - also backwards compatibility for Mainnet return isUpdatedMajor && (Number(patch) >= 5 || netId !== NetId.MAINNET); // Patch checking - also backwards compatibility for Mainnet
} }
**/ **/
export declare function calculateScore({ stakeBalance, tornadoServiceFee }: RelayerInfo): bigint; export declare function calculateScore({ stakeBalance, tornadoServiceFee, }: RelayerInfo): bigint;
export declare function getWeightRandom(weightsScores: bigint[], random: bigint): number; export declare function getWeightRandom(weightsScores: bigint[], random: bigint): number;
export interface RelayerInstanceList { export interface RelayerInstanceList {
[key: string]: { [key: string]: {

1384
dist/tornado.umd.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@ import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-ethers'; import '@nomicfoundation/hardhat-ethers';
const config: HardhatUserConfig = { const config: HardhatUserConfig = {
solidity: '0.8.28', solidity: '0.8.28',
}; };
export default config; export default config;

View File

@@ -1,93 +1,93 @@
{ {
"name": "@tornado/core", "name": "@tornado/core",
"version": "1.0.19", "version": "1.0.19",
"description": "An SDK for building applications on top of Privacy Pools", "description": "An SDK for building applications on top of Privacy Pools",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"unpkg": "./dist/tornado.umd.min.js", "unpkg": "./dist/tornado.umd.min.js",
"jsdelivr": "./dist/tornado.umd.min.js", "jsdelivr": "./dist/tornado.umd.min.js",
"scripts": { "scripts": {
"typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json", "typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json",
"types": "tsc --declaration --emitDeclarationOnly -p tsconfig.build.json", "types": "tsc --declaration --emitDeclarationOnly -p tsconfig.build.json",
"lint": "eslint src/**/*.ts test/**/*.ts --ext .ts --ignore-pattern src/typechain", "lint": "eslint src/**/*.ts test/**/*.ts --ext .ts --ignore-pattern src/typechain",
"build:node": "rollup -c", "build:node": "rollup -c",
"build:web": "webpack", "build:web": "webpack",
"build": "yarn types && yarn build:node && yarn build:web", "build": "yarn types && yarn build:node && yarn build:web",
"test": "nyc mocha --require ts-node/register --require source-map-support/register --recursive 'test/**/*.ts' --timeout '300000'" "test": "nyc mocha --require ts-node/register --require source-map-support/register --recursive 'test/**/*.ts' --timeout '300000'"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"files": [ "files": [
"dist", "dist",
"src", "src",
".eslintrc.js", ".eslintrc.js",
".gitattributes", ".gitattributes",
".gitignore", ".gitignore",
".npmrc", ".npmrc",
"logo.png", "logo.png",
"logo2.png", "logo2.png",
"rollup.config.mjs", "rollup.config.mjs",
"tsconfig.json", "tsconfig.json",
"yarn.lock" "yarn.lock"
], ],
"dependencies": { "dependencies": {
"@metamask/eth-sig-util": "^8.0.0", "@metamask/eth-sig-util": "^8.0.0",
"@tornado/contracts": "git+https://git.tornado.ws/tornadocontrib/tornado-contracts.git#1b1d707878c16a3dc60d295299d4f0e7ce6ba831", "@tornado/contracts": "git+https://git.tornado.ws/tornadocontrib/tornado-contracts.git#1b1d707878c16a3dc60d295299d4f0e7ce6ba831",
"@tornado/fixed-merkle-tree": "^0.7.3", "@tornado/fixed-merkle-tree": "^0.7.3",
"@tornado/snarkjs": "^0.1.20", "@tornado/snarkjs": "^0.1.20",
"@tornado/websnark": "^0.0.4", "@tornado/websnark": "^0.0.4",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"bn.js": "^5.2.1", "bn.js": "^5.2.1",
"circomlibjs": "0.1.7", "circomlibjs": "0.1.7",
"cross-fetch": "^4.0.0", "cross-fetch": "^4.0.0",
"ethers": "^6.13.4", "ethers": "^6.13.4",
"ffjavascript": "0.2.48", "ffjavascript": "0.2.48",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"idb": "^8.0.0" "idb": "^8.0.0"
}, },
"devDependencies": { "devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7", "@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
"@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomicfoundation/hardhat-ethers": "^3.0.8",
"@nomicfoundation/hardhat-ignition": "^0.15.5", "@nomicfoundation/hardhat-ignition": "^0.15.5",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.5", "@nomicfoundation/hardhat-ignition-ethers": "^0.15.5",
"@nomicfoundation/hardhat-network-helpers": "^1.0.11", "@nomicfoundation/hardhat-network-helpers": "^1.0.11",
"@nomicfoundation/hardhat-toolbox": "^5.0.0", "@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.10", "@nomicfoundation/hardhat-verify": "^2.0.10",
"@nomicfoundation/ignition-core": "^0.15.5", "@nomicfoundation/ignition-core": "^0.15.5",
"@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-node-resolve": "^15.3.0",
"@typechain/ethers-v6": "^0.5.1", "@typechain/ethers-v6": "^0.5.1",
"@typechain/hardhat": "^9.1.0", "@typechain/hardhat": "^9.1.0",
"@types/bn.js": "^5.1.6", "@types/bn.js": "^5.1.6",
"@types/chai": "^4.2.0", "@types/chai": "^4.2.0",
"@types/circomlibjs": "^0.1.6", "@types/circomlibjs": "^0.1.6",
"@types/mocha": "^10.0.9", "@types/mocha": "^10.0.9",
"@types/node": "^22.8.0", "@types/node": "^22.8.0",
"@types/node-fetch": "^2.6.11", "@types/node-fetch": "^2.6.11",
"@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/eslint-plugin": "^8.11.0",
"@typescript-eslint/parser": "^8.11.0", "@typescript-eslint/parser": "^8.11.0",
"esbuild-loader": "^4.2.2", "esbuild-loader": "^4.2.2",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3", "eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"hardhat": "^2.22.10", "hardhat": "^2.22.10",
"hardhat-gas-reporter": "^2.2.1", "hardhat-gas-reporter": "^2.2.1",
"mocha": "^10.7.3", "mocha": "^10.7.3",
"node-polyfill-webpack-plugin": "^4.0.0", "node-polyfill-webpack-plugin": "^4.0.0",
"nyc": "^17.1.0", "nyc": "^17.1.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"rollup": "^4.24.0", "rollup": "^4.24.0",
"rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1",
"solidity-coverage": "^0.8.13", "solidity-coverage": "^0.8.13",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsc": "^2.0.4", "tsc": "^2.0.4",
"typechain": "^8.3.2", "typechain": "^8.3.2",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"webpack": "^5.95.0", "webpack": "^5.95.0",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
} }
} }

View File

@@ -7,81 +7,81 @@ import { readFileSync } from 'fs';
const pkgJson = JSON.parse(readFileSync("./package.json")); const pkgJson = JSON.parse(readFileSync("./package.json"));
const external = Object.keys(pkgJson.dependencies).concat( const external = Object.keys(pkgJson.dependencies).concat(
Object.keys(pkgJson.optionalDependencies || {}), Object.keys(pkgJson.optionalDependencies || {}),
[ [
'http-proxy-agent', 'http-proxy-agent',
'https-proxy-agent', 'https-proxy-agent',
'socks-proxy-agent', 'socks-proxy-agent',
'@tornado/websnark/src/utils', '@tornado/websnark/src/utils',
'@tornado/websnark/src/groth16', '@tornado/websnark/src/groth16',
] ]
); );
const config = [ const config = [
{ {
input: 'src/index.ts', input: 'src/index.ts',
output: [ output: [
{ {
file: pkgJson.main, file: pkgJson.main,
format: "cjs", format: "cjs",
esModule: false, esModule: false,
}, },
], ],
external, external,
plugins: [ plugins: [
esbuild({ esbuild({
include: /\.[jt]sx?$/, include: /\.[jt]sx?$/,
minify: false, minify: false,
sourceMap: true, sourceMap: true,
target: 'es2022', target: 'es2022',
}), }),
commonjs(), commonjs(),
nodeResolve(), nodeResolve(),
json() json()
], ],
}, },
{ {
input: 'src/index.ts', input: 'src/index.ts',
output: [ output: [
{ {
file: pkgJson.module, file: pkgJson.module,
format: "esm", format: "esm",
}, },
], ],
external, external,
plugins: [ plugins: [
esbuild({ esbuild({
include: /\.[jt]sx?$/, include: /\.[jt]sx?$/,
minify: false, minify: false,
sourceMap: true, sourceMap: true,
target: 'es2022', target: 'es2022',
}), }),
nodeResolve(), nodeResolve(),
json() json()
], ],
}, },
{ {
input: 'src/merkleTreeWorker.ts', input: 'src/merkleTreeWorker.ts',
output: [ output: [
{ {
file: 'dist/merkleTreeWorker.js', file: 'dist/merkleTreeWorker.js',
format: "cjs", format: "cjs",
esModule: false, esModule: false,
}, },
], ],
treeshake: 'smallest', treeshake: 'smallest',
plugins: [ plugins: [
esbuild({ esbuild({
include: /\.[jt]sx?$/, include: /\.[jt]sx?$/,
minify: false, minify: false,
sourceMap: true, sourceMap: true,
target: 'es2022', target: 'es2022',
}), }),
commonjs(), commonjs(),
nodeResolve(), nodeResolve(),
json() json()
], ],
} }
] ]
export default config; export default config;

View File

@@ -1,339 +1,399 @@
import type { Provider, BlockTag, Block, TransactionResponse, BaseContract, ContractEventName, EventLog } from 'ethers'; import type {
Provider,
BlockTag,
Block,
TransactionResponse,
BaseContract,
ContractEventName,
EventLog,
} from 'ethers';
import { chunk, sleep } from './utils'; import { chunk, sleep } from './utils';
export interface BatchBlockServiceConstructor { export interface BatchBlockServiceConstructor {
provider: Provider; provider: Provider;
onProgress?: BatchBlockOnProgress; onProgress?: BatchBlockOnProgress;
concurrencySize?: number; concurrencySize?: number;
batchSize?: number; batchSize?: number;
shouldRetry?: boolean; shouldRetry?: boolean;
retryMax?: number; retryMax?: number;
retryOn?: number; retryOn?: number;
} }
export type BatchBlockOnProgress = ({ export type BatchBlockOnProgress = ({
percentage, percentage,
currentIndex, currentIndex,
totalIndex, totalIndex,
}: { }: {
percentage: number; percentage: number;
currentIndex?: number; currentIndex?: number;
totalIndex?: number; totalIndex?: number;
}) => void; }) => void;
/** /**
* Fetch blocks from web3 provider on batches * Fetch blocks from web3 provider on batches
*/ */
export class BatchBlockService { export class BatchBlockService {
provider: Provider; provider: Provider;
onProgress?: BatchBlockOnProgress; onProgress?: BatchBlockOnProgress;
concurrencySize: number; concurrencySize: number;
batchSize: number; batchSize: number;
shouldRetry: boolean; shouldRetry: boolean;
retryMax: number; retryMax: number;
retryOn: number; retryOn: number;
constructor({ constructor({
provider, provider,
onProgress, onProgress,
concurrencySize = 10, concurrencySize = 10,
batchSize = 10, batchSize = 10,
shouldRetry = true, shouldRetry = true,
retryMax = 5, retryMax = 5,
retryOn = 500, retryOn = 500,
}: BatchBlockServiceConstructor) { }: BatchBlockServiceConstructor) {
this.provider = provider; this.provider = provider;
this.onProgress = onProgress; this.onProgress = onProgress;
this.concurrencySize = concurrencySize; this.concurrencySize = concurrencySize;
this.batchSize = batchSize; this.batchSize = batchSize;
this.shouldRetry = shouldRetry; this.shouldRetry = shouldRetry;
this.retryMax = retryMax; this.retryMax = retryMax;
this.retryOn = retryOn; this.retryOn = retryOn;
}
async getBlock(blockTag: BlockTag): Promise<Block> {
const blockObject = await this.provider.getBlock(blockTag);
// if the provider returns null (which they have corrupted block data for one of their nodes) throw and retry
if (!blockObject) {
const errMsg = `No block for ${blockTag}`;
throw new Error(errMsg);
} }
return blockObject; async getBlock(blockTag: BlockTag): Promise<Block> {
} const blockObject = await this.provider.getBlock(blockTag);
createBatchRequest(batchArray: BlockTag[][]): Promise<Block[]>[] { // if the provider returns null (which they have corrupted block data for one of their nodes) throw and retry
return batchArray.map(async (blocks: BlockTag[], index: number) => { if (!blockObject) {
// send batch requests on milliseconds to avoid including them on a single batch request const errMsg = `No block for ${blockTag}`;
await sleep(20 * index); throw new Error(errMsg);
return (async () => {
let retries = 0;
let err;
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
try {
return await Promise.all(blocks.map((b) => this.getBlock(b)));
} catch (e) {
retries++;
err = e;
// retry on 0.5 seconds
await sleep(this.retryOn);
}
} }
throw err; return blockObject;
})();
});
}
async getBatchBlocks(blocks: BlockTag[]): Promise<Block[]> {
let blockCount = 0;
const results: Block[] = [];
for (const chunks of chunk(blocks, this.concurrencySize * this.batchSize)) {
const chunksResult = (await Promise.all(this.createBatchRequest(chunk(chunks, this.batchSize)))).flat();
results.push(...chunksResult);
blockCount += chunks.length;
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: blockCount / blocks.length,
currentIndex: blockCount,
totalIndex: blocks.length,
});
}
} }
return results; createBatchRequest(batchArray: BlockTag[][]): Promise<Block[]>[] {
} return batchArray.map(async (blocks: BlockTag[], index: number) => {
// send batch requests on milliseconds to avoid including them on a single batch request
await sleep(20 * index);
return (async () => {
let retries = 0;
let err;
// eslint-disable-next-line no-unmodified-loop-condition
while (
(!this.shouldRetry && retries === 0) ||
(this.shouldRetry && retries < this.retryMax)
) {
try {
return await Promise.all(
blocks.map((b) => this.getBlock(b)),
);
} catch (e) {
retries++;
err = e;
// retry on 0.5 seconds
await sleep(this.retryOn);
}
}
throw err;
})();
});
}
async getBatchBlocks(blocks: BlockTag[]): Promise<Block[]> {
let blockCount = 0;
const results: Block[] = [];
for (const chunks of chunk(
blocks,
this.concurrencySize * this.batchSize,
)) {
const chunksResult = (
await Promise.all(
this.createBatchRequest(chunk(chunks, this.batchSize)),
)
).flat();
results.push(...chunksResult);
blockCount += chunks.length;
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: blockCount / blocks.length,
currentIndex: blockCount,
totalIndex: blocks.length,
});
}
}
return results;
}
} }
/** /**
* Fetch transactions from web3 provider on batches * Fetch transactions from web3 provider on batches
*/ */
export class BatchTransactionService { export class BatchTransactionService {
provider: Provider; provider: Provider;
onProgress?: BatchBlockOnProgress; onProgress?: BatchBlockOnProgress;
concurrencySize: number; concurrencySize: number;
batchSize: number; batchSize: number;
shouldRetry: boolean; shouldRetry: boolean;
retryMax: number; retryMax: number;
retryOn: number; retryOn: number;
constructor({ constructor({
provider, provider,
onProgress, onProgress,
concurrencySize = 10, concurrencySize = 10,
batchSize = 10, batchSize = 10,
shouldRetry = true, shouldRetry = true,
retryMax = 5, retryMax = 5,
retryOn = 500, retryOn = 500,
}: BatchBlockServiceConstructor) { }: BatchBlockServiceConstructor) {
this.provider = provider; this.provider = provider;
this.onProgress = onProgress; this.onProgress = onProgress;
this.concurrencySize = concurrencySize; this.concurrencySize = concurrencySize;
this.batchSize = batchSize; this.batchSize = batchSize;
this.shouldRetry = shouldRetry; this.shouldRetry = shouldRetry;
this.retryMax = retryMax; this.retryMax = retryMax;
this.retryOn = retryOn; this.retryOn = retryOn;
}
async getTransaction(txHash: string): Promise<TransactionResponse> {
const txObject = await this.provider.getTransaction(txHash);
if (!txObject) {
const errMsg = `No transaction for ${txHash}`;
throw new Error(errMsg);
} }
return txObject; async getTransaction(txHash: string): Promise<TransactionResponse> {
} const txObject = await this.provider.getTransaction(txHash);
createBatchRequest(batchArray: string[][]): Promise<TransactionResponse[]>[] { if (!txObject) {
return batchArray.map(async (txs: string[], index: number) => { const errMsg = `No transaction for ${txHash}`;
await sleep(20 * index); throw new Error(errMsg);
return (async () => {
let retries = 0;
let err;
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
try {
return await Promise.all(txs.map((tx) => this.getTransaction(tx)));
} catch (e) {
retries++;
err = e;
// retry on 0.5 seconds
await sleep(this.retryOn);
}
} }
throw err; return txObject;
})();
});
}
async getBatchTransactions(txs: string[]): Promise<TransactionResponse[]> {
let txCount = 0;
const results = [];
for (const chunks of chunk(txs, this.concurrencySize * this.batchSize)) {
const chunksResult = (await Promise.all(this.createBatchRequest(chunk(chunks, this.batchSize)))).flat();
results.push(...chunksResult);
txCount += chunks.length;
if (typeof this.onProgress === 'function') {
this.onProgress({ percentage: txCount / txs.length, currentIndex: txCount, totalIndex: txs.length });
}
} }
return results; createBatchRequest(
} batchArray: string[][],
): Promise<TransactionResponse[]>[] {
return batchArray.map(async (txs: string[], index: number) => {
await sleep(20 * index);
return (async () => {
let retries = 0;
let err;
// eslint-disable-next-line no-unmodified-loop-condition
while (
(!this.shouldRetry && retries === 0) ||
(this.shouldRetry && retries < this.retryMax)
) {
try {
return await Promise.all(
txs.map((tx) => this.getTransaction(tx)),
);
} catch (e) {
retries++;
err = e;
// retry on 0.5 seconds
await sleep(this.retryOn);
}
}
throw err;
})();
});
}
async getBatchTransactions(txs: string[]): Promise<TransactionResponse[]> {
let txCount = 0;
const results = [];
for (const chunks of chunk(
txs,
this.concurrencySize * this.batchSize,
)) {
const chunksResult = (
await Promise.all(
this.createBatchRequest(chunk(chunks, this.batchSize)),
)
).flat();
results.push(...chunksResult);
txCount += chunks.length;
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: txCount / txs.length,
currentIndex: txCount,
totalIndex: txs.length,
});
}
}
return results;
}
} }
export interface BatchEventServiceConstructor { export interface BatchEventServiceConstructor {
provider: Provider; provider: Provider;
contract: BaseContract; contract: BaseContract;
onProgress?: BatchEventOnProgress; onProgress?: BatchEventOnProgress;
concurrencySize?: number; concurrencySize?: number;
blocksPerRequest?: number; blocksPerRequest?: number;
shouldRetry?: boolean; shouldRetry?: boolean;
retryMax?: number; retryMax?: number;
retryOn?: number; retryOn?: number;
} }
export type BatchEventOnProgress = ({ export type BatchEventOnProgress = ({
percentage, percentage,
type, type,
fromBlock, fromBlock,
toBlock, toBlock,
count, count,
}: { }: {
percentage: number; percentage: number;
type?: ContractEventName; type?: ContractEventName;
fromBlock?: number; fromBlock?: number;
toBlock?: number; toBlock?: number;
count?: number; count?: number;
}) => void; }) => void;
// To enable iteration only numbers are accepted for fromBlock input // To enable iteration only numbers are accepted for fromBlock input
export interface EventInput { export interface EventInput {
fromBlock: number; fromBlock: number;
toBlock: number; toBlock: number;
type: ContractEventName; type: ContractEventName;
} }
/** /**
* Fetch events from web3 provider on bulk * Fetch events from web3 provider on bulk
*/ */
export class BatchEventsService { export class BatchEventsService {
provider: Provider; provider: Provider;
contract: BaseContract; contract: BaseContract;
onProgress?: BatchEventOnProgress; onProgress?: BatchEventOnProgress;
concurrencySize: number; concurrencySize: number;
blocksPerRequest: number; blocksPerRequest: number;
shouldRetry: boolean; shouldRetry: boolean;
retryMax: number; retryMax: number;
retryOn: number; retryOn: number;
constructor({ constructor({
provider, provider,
contract, contract,
onProgress, onProgress,
concurrencySize = 10, concurrencySize = 10,
blocksPerRequest = 2000, blocksPerRequest = 2000,
shouldRetry = true, shouldRetry = true,
retryMax = 5, retryMax = 5,
retryOn = 500, retryOn = 500,
}: BatchEventServiceConstructor) { }: BatchEventServiceConstructor) {
this.provider = provider; this.provider = provider;
this.contract = contract; this.contract = contract;
this.onProgress = onProgress; this.onProgress = onProgress;
this.concurrencySize = concurrencySize; this.concurrencySize = concurrencySize;
this.blocksPerRequest = blocksPerRequest; this.blocksPerRequest = blocksPerRequest;
this.shouldRetry = shouldRetry; this.shouldRetry = shouldRetry;
this.retryMax = retryMax; this.retryMax = retryMax;
this.retryOn = retryOn; this.retryOn = retryOn;
} }
async getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]> { async getPastEvents({
let err; fromBlock,
let retries = 0; toBlock,
type,
}: EventInput): Promise<EventLog[]> {
let err;
let retries = 0;
// eslint-disable-next-line no-unmodified-loop-condition // eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) { while (
try { (!this.shouldRetry && retries === 0) ||
return (await this.contract.queryFilter(type, fromBlock, toBlock)) as EventLog[]; (this.shouldRetry && retries < this.retryMax)
// eslint-disable-next-line @typescript-eslint/no-explicit-any ) {
} catch (e: any) { try {
err = e; return (await this.contract.queryFilter(
retries++; type,
fromBlock,
toBlock,
)) as EventLog[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
err = e;
retries++;
// If provider.getBlockNumber returned last block that isn't accepted (happened on Avalanche/Gnosis), // If provider.getBlockNumber returned last block that isn't accepted (happened on Avalanche/Gnosis),
// get events to last accepted block // get events to last accepted block
if (e.message.includes('after last accepted block')) { if (e.message.includes('after last accepted block')) {
const acceptedBlock = parseInt(e.message.split('after last accepted block ')[1]); const acceptedBlock = parseInt(
toBlock = acceptedBlock; e.message.split('after last accepted block ')[1],
);
toBlock = acceptedBlock;
}
// retry on 0.5 seconds
await sleep(this.retryOn);
}
} }
// retry on 0.5 seconds throw err;
await sleep(this.retryOn);
}
} }
throw err; createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[] {
} return batchArray.map(async (event: EventInput, index: number) => {
await sleep(20 * index);
createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[] { return this.getPastEvents(event);
return batchArray.map(async (event: EventInput, index: number) => {
await sleep(20 * index);
return this.getPastEvents(event);
});
}
async getBatchEvents({ fromBlock, toBlock, type = '*' }: EventInput): Promise<EventLog[]> {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
const eventsToSync = [];
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1;
eventsToSync.push({ fromBlock: i, toBlock: j, type });
}
const events = [];
const eventChunk = chunk(eventsToSync, this.concurrencySize);
let chunkCount = 0;
for (const chunk of eventChunk) {
chunkCount++;
const fetchedEvents = (await Promise.all(this.createBatchRequest(chunk))).flat();
events.push(...fetchedEvents);
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: chunkCount / eventChunk.length,
type,
fromBlock: chunk[0].fromBlock,
toBlock: chunk[chunk.length - 1].toBlock,
count: fetchedEvents.length,
}); });
}
} }
return events; async getBatchEvents({
} fromBlock,
toBlock,
type = '*',
}: EventInput): Promise<EventLog[]> {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
const eventsToSync = [];
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
const j =
i + this.blocksPerRequest - 1 > toBlock
? toBlock
: i + this.blocksPerRequest - 1;
eventsToSync.push({ fromBlock: i, toBlock: j, type });
}
const events = [];
const eventChunk = chunk(eventsToSync, this.concurrencySize);
let chunkCount = 0;
for (const chunk of eventChunk) {
chunkCount++;
const fetchedEvents = (
await Promise.all(this.createBatchRequest(chunk))
).flat();
events.push(...fetchedEvents);
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: chunkCount / eventChunk.length,
type,
fromBlock: chunk[0].fromBlock,
toBlock: chunk[chunk.length - 1].toBlock,
count: fetchedEvents.length,
});
}
}
return events;
}
} }

View File

@@ -1,17 +1,17 @@
export * from '@tornado/contracts'; export * from '@tornado/contracts';
export { export {
Multicall, Multicall,
Multicall__factory, Multicall__factory,
OffchainOracle, OffchainOracle,
OffchainOracle__factory, OffchainOracle__factory,
OvmGasPriceOracle, OvmGasPriceOracle,
OvmGasPriceOracle__factory, OvmGasPriceOracle__factory,
ReverseRecords, ReverseRecords,
ReverseRecords__factory, ReverseRecords__factory,
ENSNameWrapper, ENSNameWrapper,
ENSNameWrapper__factory, ENSNameWrapper__factory,
ENSRegistry, ENSRegistry,
ENSRegistry__factory, ENSRegistry__factory,
ENSResolver, ENSResolver,
ENSResolver__factory, ENSResolver__factory,
} from './typechain'; } from './typechain';

View File

@@ -1,269 +1,303 @@
import { bnToBytes, bytesToBN, leBuff2Int, leInt2Buff, rBigInt, toFixedHex } from './utils'; import {
bnToBytes,
bytesToBN,
leBuff2Int,
leInt2Buff,
rBigInt,
toFixedHex,
} from './utils';
import { buffPedersenHash } from './pedersen'; import { buffPedersenHash } from './pedersen';
import type { NetIdType } from './networkConfig'; import type { NetIdType } from './networkConfig';
export interface DepositType { export interface DepositType {
currency: string; currency: string;
amount: string; amount: string;
netId: NetIdType; netId: NetIdType;
} }
export interface createDepositParams { export interface createDepositParams {
nullifier: bigint; nullifier: bigint;
secret: bigint; secret: bigint;
} }
export interface createDepositObject { export interface createDepositObject {
preimage: Uint8Array; preimage: Uint8Array;
noteHex: string; noteHex: string;
commitment: bigint; commitment: bigint;
commitmentHex: string; commitmentHex: string;
nullifierHash: bigint; nullifierHash: bigint;
nullifierHex: string; nullifierHex: string;
} }
export interface createNoteParams extends DepositType { export interface createNoteParams extends DepositType {
nullifier?: bigint; nullifier?: bigint;
secret?: bigint; secret?: bigint;
} }
export interface parsedNoteExec extends DepositType { export interface parsedNoteExec extends DepositType {
note: string; note: string;
noteHex: string; noteHex: string;
} }
export interface parsedInvoiceExec extends DepositType { export interface parsedInvoiceExec extends DepositType {
invoice: string; invoice: string;
commitmentHex: string; commitmentHex: string;
} }
export function parseNote(noteString: string): parsedNoteExec | undefined { export function parseNote(noteString: string): parsedNoteExec | undefined {
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<noteHex>[0-9a-fA-F]{124})/g; const noteRegex =
const match = noteRegex.exec(noteString); /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<noteHex>[0-9a-fA-F]{124})/g;
if (!match) { const match = noteRegex.exec(noteString);
return; if (!match) {
} return;
}
const { currency, amount, netId, noteHex } = match.groups as unknown as parsedNoteExec; const { currency, amount, netId, noteHex } =
match.groups as unknown as parsedNoteExec;
return { return {
currency: currency.toLowerCase(), currency: currency.toLowerCase(),
amount, amount,
netId: Number(netId), netId: Number(netId),
noteHex: '0x' + noteHex, noteHex: '0x' + noteHex,
note: noteString, note: noteString,
}; };
} }
export function parseInvoice(invoiceString: string): parsedInvoiceExec | undefined { export function parseInvoice(
const invoiceRegex = invoiceString: string,
/tornadoInvoice-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<commitmentHex>[0-9a-fA-F]{64})/g; ): parsedInvoiceExec | undefined {
const match = invoiceRegex.exec(invoiceString); const invoiceRegex =
if (!match) { /tornadoInvoice-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<commitmentHex>[0-9a-fA-F]{64})/g;
return; const match = invoiceRegex.exec(invoiceString);
} if (!match) {
return;
}
const { currency, amount, netId, commitmentHex } = match.groups as unknown as parsedInvoiceExec; const { currency, amount, netId, commitmentHex } =
match.groups as unknown as parsedInvoiceExec;
return { return {
currency: currency.toLowerCase(), currency: currency.toLowerCase(),
amount, amount,
netId: Number(netId), netId: Number(netId),
commitmentHex: '0x' + commitmentHex, commitmentHex: '0x' + commitmentHex,
invoice: invoiceString, invoice: invoiceString,
}; };
} }
export async function createDeposit({ nullifier, secret }: createDepositParams): Promise<createDepositObject> { export async function createDeposit({
const preimage = new Uint8Array([...leInt2Buff(nullifier), ...leInt2Buff(secret)]); nullifier,
const noteHex = toFixedHex(bytesToBN(preimage), 62); secret,
const commitment = BigInt(await buffPedersenHash(preimage)); }: createDepositParams): Promise<createDepositObject> {
const commitmentHex = toFixedHex(commitment); const preimage = new Uint8Array([
const nullifierHash = BigInt(await buffPedersenHash(leInt2Buff(nullifier))); ...leInt2Buff(nullifier),
const nullifierHex = toFixedHex(nullifierHash); ...leInt2Buff(secret),
]);
const noteHex = toFixedHex(bytesToBN(preimage), 62);
const commitment = BigInt(await buffPedersenHash(preimage));
const commitmentHex = toFixedHex(commitment);
const nullifierHash = BigInt(await buffPedersenHash(leInt2Buff(nullifier)));
const nullifierHex = toFixedHex(nullifierHash);
return { return {
preimage, preimage,
noteHex, noteHex,
commitment, commitment,
commitmentHex, commitmentHex,
nullifierHash, nullifierHash,
nullifierHex, nullifierHex,
}; };
} }
export interface DepositConstructor { export interface DepositConstructor {
currency: string; currency: string;
amount: string; amount: string;
netId: NetIdType; netId: NetIdType;
nullifier: bigint; nullifier: bigint;
secret: bigint; secret: bigint;
note: string; note: string;
noteHex: string; noteHex: string;
invoice: string; invoice: string;
commitmentHex: string; commitmentHex: string;
nullifierHex: string; nullifierHex: string;
} }
export class Deposit { export class Deposit {
currency: string; currency: string;
amount: string; amount: string;
netId: NetIdType; netId: NetIdType;
nullifier: bigint; nullifier: bigint;
secret: bigint; secret: bigint;
note: string; note: string;
noteHex: string; noteHex: string;
invoice: string; invoice: string;
commitmentHex: string; commitmentHex: string;
nullifierHex: string; nullifierHex: string;
constructor({ constructor({
currency, currency,
amount, amount,
netId, netId,
nullifier, nullifier,
secret, secret,
note, note,
noteHex, noteHex,
invoice, invoice,
commitmentHex, commitmentHex,
nullifierHex, nullifierHex,
}: DepositConstructor) { }: DepositConstructor) {
this.currency = currency; this.currency = currency;
this.amount = amount; this.amount = amount;
this.netId = netId; this.netId = netId;
this.nullifier = nullifier; this.nullifier = nullifier;
this.secret = secret; this.secret = secret;
this.note = note; this.note = note;
this.noteHex = noteHex; this.noteHex = noteHex;
this.invoice = invoice; this.invoice = invoice;
this.commitmentHex = commitmentHex; this.commitmentHex = commitmentHex;
this.nullifierHex = nullifierHex; this.nullifierHex = nullifierHex;
}
toString() {
return JSON.stringify(
{
currency: this.currency,
amount: this.amount,
netId: this.netId,
nullifier: this.nullifier,
secret: this.secret,
note: this.note,
noteHex: this.noteHex,
invoice: this.invoice,
commitmentHex: this.commitmentHex,
nullifierHex: this.nullifierHex,
},
null,
2,
);
}
static async createNote({ currency, amount, netId, nullifier, secret }: createNoteParams): Promise<Deposit> {
if (!nullifier) {
nullifier = rBigInt(31);
}
if (!secret) {
secret = rBigInt(31);
} }
const depositObject = await createDeposit({ toString() {
nullifier, return JSON.stringify(
secret, {
}); currency: this.currency,
amount: this.amount,
const newDeposit = new Deposit({ netId: this.netId,
currency: currency.toLowerCase(), nullifier: this.nullifier,
amount: amount, secret: this.secret,
netId, note: this.note,
note: `tornado-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.noteHex}`, noteHex: this.noteHex,
noteHex: depositObject.noteHex, invoice: this.invoice,
invoice: `tornadoInvoice-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.commitmentHex}`, commitmentHex: this.commitmentHex,
nullifier: nullifier, nullifierHex: this.nullifierHex,
secret: secret, },
commitmentHex: depositObject.commitmentHex, null,
nullifierHex: depositObject.nullifierHex, 2,
}); );
return newDeposit;
}
static async parseNote(noteString: string): Promise<Deposit> {
const parsedNote = parseNote(noteString);
if (!parsedNote) {
throw new Error('The note has invalid format');
} }
const { currency, amount, netId, note, noteHex: parsedNoteHex } = parsedNote; static async createNote({
currency,
amount,
netId,
nullifier,
secret,
}: createNoteParams): Promise<Deposit> {
if (!nullifier) {
nullifier = rBigInt(31);
}
if (!secret) {
secret = rBigInt(31);
}
const bytes = bnToBytes(parsedNoteHex); const depositObject = await createDeposit({
const nullifier = BigInt(leBuff2Int(bytes.slice(0, 31)).toString()); nullifier,
const secret = BigInt(leBuff2Int(bytes.slice(31, 62)).toString()); secret,
});
const { noteHex, commitmentHex, nullifierHex } = await createDeposit({ nullifier, secret }); const newDeposit = new Deposit({
currency: currency.toLowerCase(),
amount: amount,
netId,
note: `tornado-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.noteHex}`,
noteHex: depositObject.noteHex,
invoice: `tornadoInvoice-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.commitmentHex}`,
nullifier: nullifier,
secret: secret,
commitmentHex: depositObject.commitmentHex,
nullifierHex: depositObject.nullifierHex,
});
const invoice = `tornadoInvoice-${currency}-${amount}-${netId}-${commitmentHex}`; return newDeposit;
}
const newDeposit = new Deposit({ static async parseNote(noteString: string): Promise<Deposit> {
currency, const parsedNote = parseNote(noteString);
amount,
netId,
note,
noteHex,
invoice,
nullifier,
secret,
commitmentHex,
nullifierHex,
});
return newDeposit; if (!parsedNote) {
} throw new Error('The note has invalid format');
}
const {
currency,
amount,
netId,
note,
noteHex: parsedNoteHex,
} = parsedNote;
const bytes = bnToBytes(parsedNoteHex);
const nullifier = BigInt(leBuff2Int(bytes.slice(0, 31)).toString());
const secret = BigInt(leBuff2Int(bytes.slice(31, 62)).toString());
const { noteHex, commitmentHex, nullifierHex } = await createDeposit({
nullifier,
secret,
});
const invoice = `tornadoInvoice-${currency}-${amount}-${netId}-${commitmentHex}`;
const newDeposit = new Deposit({
currency,
amount,
netId,
note,
noteHex,
invoice,
nullifier,
secret,
commitmentHex,
nullifierHex,
});
return newDeposit;
}
} }
export class Invoice { export class Invoice {
currency: string; currency: string;
amount: string; amount: string;
netId: NetIdType; netId: NetIdType;
commitmentHex: string; commitmentHex: string;
invoice: string; invoice: string;
constructor(invoiceString: string) { constructor(invoiceString: string) {
const parsedInvoice = parseInvoice(invoiceString); const parsedInvoice = parseInvoice(invoiceString);
if (!parsedInvoice) { if (!parsedInvoice) {
throw new Error('The invoice has invalid format'); throw new Error('The invoice has invalid format');
}
const { currency, amount, netId, invoice, commitmentHex } =
parsedInvoice;
this.currency = currency;
this.amount = amount;
this.netId = netId;
this.commitmentHex = commitmentHex;
this.invoice = invoice;
} }
const { currency, amount, netId, invoice, commitmentHex } = parsedInvoice; toString() {
return JSON.stringify(
this.currency = currency; {
this.amount = amount; currency: this.currency,
this.netId = netId; amount: this.amount,
netId: this.netId,
this.commitmentHex = commitmentHex; commitmentHex: this.commitmentHex,
this.invoice = invoice; invoice: this.invoice,
} },
null,
toString() { 2,
return JSON.stringify( );
{ }
currency: this.currency,
amount: this.amount,
netId: this.netId,
commitmentHex: this.commitmentHex,
invoice: this.invoice,
},
null,
2,
);
}
} }

View File

@@ -1,213 +1,257 @@
import { getEncryptionPublicKey, encrypt, decrypt, EthEncryptedData } from '@metamask/eth-sig-util'; import {
import { JsonRpcApiProvider, Signer, Wallet, computeAddress, getAddress } from 'ethers'; getEncryptionPublicKey,
import { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes, rHex } from './utils'; encrypt,
decrypt,
EthEncryptedData,
} from '@metamask/eth-sig-util';
import {
JsonRpcApiProvider,
Signer,
Wallet,
computeAddress,
getAddress,
} from 'ethers';
import {
base64ToBytes,
bytesToBase64,
bytesToHex,
hexToBytes,
toFixedHex,
concatBytes,
rHex,
} from './utils';
import { EchoEvents, EncryptedNotesEvents } from './events'; import { EchoEvents, EncryptedNotesEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface NoteToEncrypt { export interface NoteToEncrypt {
address: string; address: string;
noteHex: string; noteHex: string;
} }
export interface DecryptedNotes { export interface DecryptedNotes {
blockNumber: number; blockNumber: number;
address: string; address: string;
noteHex: string; noteHex: string;
} }
export function packEncryptedMessage({ nonce, ephemPublicKey, ciphertext }: EthEncryptedData) { export function packEncryptedMessage({
const nonceBuf = toFixedHex(bytesToHex(base64ToBytes(nonce)), 24); nonce,
const ephemPublicKeyBuf = toFixedHex(bytesToHex(base64ToBytes(ephemPublicKey)), 32); ephemPublicKey,
const ciphertextBuf = bytesToHex(base64ToBytes(ciphertext)); 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)); const messageBuff = concatBytes(
hexToBytes(nonceBuf),
hexToBytes(ephemPublicKeyBuf),
hexToBytes(ciphertextBuf),
);
return bytesToHex(messageBuff); return bytesToHex(messageBuff);
} }
export function unpackEncryptedMessage(encryptedMessage: string) { export function unpackEncryptedMessage(encryptedMessage: string) {
const messageBuff = hexToBytes(encryptedMessage); const messageBuff = hexToBytes(encryptedMessage);
const nonceBuf = bytesToBase64(messageBuff.slice(0, 24)); const nonceBuf = bytesToBase64(messageBuff.slice(0, 24));
const ephemPublicKeyBuf = bytesToBase64(messageBuff.slice(24, 56)); const ephemPublicKeyBuf = bytesToBase64(messageBuff.slice(24, 56));
const ciphertextBuf = bytesToBase64(messageBuff.slice(56)); const ciphertextBuf = bytesToBase64(messageBuff.slice(56));
return { return {
messageBuff: bytesToHex(messageBuff), messageBuff: bytesToHex(messageBuff),
version: 'x25519-xsalsa20-poly1305', version: 'x25519-xsalsa20-poly1305',
nonce: nonceBuf, nonce: nonceBuf,
ephemPublicKey: ephemPublicKeyBuf, ephemPublicKey: ephemPublicKeyBuf,
ciphertext: ciphertextBuf, ciphertext: ciphertextBuf,
} as EthEncryptedData & { } as EthEncryptedData & {
messageBuff: string; messageBuff: string;
}; };
} }
export interface NoteAccountConstructor { export interface NoteAccountConstructor {
netId: NetIdType; blockNumber?: number;
blockNumber?: number; // hex
// hex recoveryKey?: string;
recoveryKey?: string;
} }
export class NoteAccount { export class NoteAccount {
netId: NetIdType; blockNumber?: number;
blockNumber?: number; // Dedicated 32 bytes private key only used for note encryption, backed up to an Echoer and local for future derivation
// 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
// Note that unlike the private key it shouldn't have the 0x prefix recoveryKey: string;
recoveryKey: string; // Address derived from recoveryKey, only used for frontend UI
// Address derived from recoveryKey, only used for frontend UI recoveryAddress: string;
recoveryAddress: string; // Note encryption public key derived from recoveryKey
// Note encryption public key derived from recoveryKey recoveryPublicKey: string;
recoveryPublicKey: string;
constructor({ netId, blockNumber, recoveryKey }: NoteAccountConstructor) { constructor({ blockNumber, recoveryKey }: NoteAccountConstructor) {
if (!recoveryKey) { if (!recoveryKey) {
recoveryKey = rHex(32).slice(2); recoveryKey = rHex(32).slice(2);
}
this.netId = Math.floor(Number(netId));
this.blockNumber = blockNumber;
this.recoveryKey = recoveryKey;
this.recoveryAddress = computeAddress('0x' + recoveryKey);
this.recoveryPublicKey = getEncryptionPublicKey(recoveryKey);
}
/**
* Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key
*/
static async getSignerPublicKey(signer: Signer | Wallet) {
if ((signer as Wallet).privateKey) {
const wallet = signer as Wallet;
const privateKey = wallet.privateKey.slice(0, 2) === '0x' ? wallet.privateKey.slice(2) : wallet.privateKey;
// Should return base64 encoded public key
return getEncryptionPublicKey(privateKey);
}
const provider = signer.provider as JsonRpcApiProvider;
return (await provider.send('eth_getEncryptionPublicKey', [
(signer as Signer & { address: string }).address,
])) as string;
}
// 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
*/
async decryptSignerNoteAccounts(signer: Signer | Wallet, events: EchoEvents[]): Promise<NoteAccount[]> {
const signerAddress = (signer as (Signer & { address: string }) | Wallet).address;
const decryptedEvents = [];
for (const event of events) {
if (event.address !== signerAddress) {
continue;
}
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount);
let recoveryKey;
if ((signer as Wallet).privateKey) {
const wallet = signer as Wallet;
const privateKey = wallet.privateKey.slice(0, 2) === '0x' ? wallet.privateKey.slice(2) : wallet.privateKey;
recoveryKey = decrypt({
encryptedData: unpackedMessage,
privateKey,
});
} else {
const { version, nonce, ephemPublicKey, ciphertext } = unpackedMessage;
const unpackedBuffer = bytesToHex(
new TextEncoder().encode(
JSON.stringify({
version,
nonce,
ephemPublicKey,
ciphertext,
}),
),
);
const provider = signer.provider as JsonRpcApiProvider;
recoveryKey = await provider.send('eth_decrypt', [unpackedBuffer, signerAddress]);
} }
decryptedEvents.push( this.blockNumber = blockNumber;
new NoteAccount({ this.recoveryKey = recoveryKey;
netId: this.netId, this.recoveryAddress = computeAddress('0x' + recoveryKey);
blockNumber: event.blockNumber, this.recoveryPublicKey = getEncryptionPublicKey(recoveryKey);
recoveryKey,
}),
);
} catch {
// decryption may fail for invalid accounts
continue;
}
} }
return decryptedEvents; /**
} * Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key
*/
static async getSignerPublicKey(signer: Signer | Wallet) {
if ((signer as Wallet).privateKey) {
const wallet = signer as Wallet;
const privateKey =
wallet.privateKey.slice(0, 2) === '0x'
? wallet.privateKey.slice(2)
: wallet.privateKey;
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[] { // Should return base64 encoded public key
const decryptedEvents = []; return getEncryptionPublicKey(privateKey);
}
for (const event of events) { const provider = signer.provider as JsonRpcApiProvider;
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedNote);
const [address, noteHex] = decrypt({ return (await provider.send('eth_getEncryptionPublicKey', [
encryptedData: unpackedMessage, (signer as Signer & { address: string }).address,
privateKey: this.recoveryKey, ])) as string;
}).split('-'); }
decryptedEvents.push({ // This function intends to provide an encrypted value of recoveryKey for an on-chain Echoer backup purpose
blockNumber: event.blockNumber, // Thus, the pubKey should be derived by a Wallet instance or from Web3 wallets
address: getAddress(address), // pubKey: base64 encoded 32 bytes key from https://docs.metamask.io/wallet/reference/eth_getencryptionpublickey/
noteHex, getEncryptedAccount(walletPublicKey: string) {
const encryptedData = encrypt({
publicKey: walletPublicKey,
data: this.recoveryKey,
version: 'x25519-xsalsa20-poly1305',
}); });
} catch {
// decryption may fail for foreign notes const data = packEncryptedMessage(encryptedData);
continue;
} 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,
};
} }
return decryptedEvents; /**
} * Decrypt Echoer backuped note encryption account with private keys
*/
static async decryptSignerNoteAccounts(
signer: Signer | Wallet,
events: EchoEvents[],
): Promise<NoteAccount[]> {
const signerAddress = (
signer as (Signer & { address: string }) | Wallet
).address;
encryptNote({ address, noteHex }: NoteToEncrypt) { const decryptedEvents = [];
const encryptedData = encrypt({
publicKey: this.recoveryPublicKey,
data: `${address}-${noteHex}`,
version: 'x25519-xsalsa20-poly1305',
});
return packEncryptedMessage(encryptedData); for (const event of events) {
} if (event.address !== signerAddress) {
continue;
}
try {
const unpackedMessage = unpackEncryptedMessage(
event.encryptedAccount,
);
let recoveryKey;
if ((signer as Wallet).privateKey) {
const wallet = signer as Wallet;
const privateKey =
wallet.privateKey.slice(0, 2) === '0x'
? wallet.privateKey.slice(2)
: wallet.privateKey;
recoveryKey = decrypt({
encryptedData: unpackedMessage,
privateKey,
});
} else {
const { version, nonce, ephemPublicKey, ciphertext } =
unpackedMessage;
const unpackedBuffer = bytesToHex(
new TextEncoder().encode(
JSON.stringify({
version,
nonce,
ephemPublicKey,
ciphertext,
}),
),
);
const provider = signer.provider as JsonRpcApiProvider;
recoveryKey = await provider.send('eth_decrypt', [
unpackedBuffer,
signerAddress,
]);
}
decryptedEvents.push(
new NoteAccount({
blockNumber: event.blockNumber,
recoveryKey,
}),
);
} 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,145 +1,175 @@
import { namehash, EnsResolver, AbstractProvider, keccak256, Signer } from 'ethers'; import {
namehash,
EnsResolver,
AbstractProvider,
keccak256,
Signer,
} from 'ethers';
import { import {
ENSNameWrapper, ENSNameWrapper,
ENSNameWrapper__factory, ENSNameWrapper__factory,
ENSRegistry, ENSRegistry,
ENSRegistry__factory, ENSRegistry__factory,
ENSResolver, ENSResolver,
ENSResolver__factory, ENSResolver__factory,
} from './typechain'; } from './typechain';
import { bytesToHex, isHex } from './utils'; import { bytesToHex, isHex } from './utils';
import { NetId, NetIdType } from './networkConfig'; import { NetId, NetIdType } from './networkConfig';
export function encodedLabelToLabelhash(label: string): string | null { export function encodedLabelToLabelhash(label: string): string | null {
if (label.length !== 66) return null; if (label.length !== 66) return null;
if (label.indexOf('[') !== 0) return null; if (label.indexOf('[') !== 0) return null;
if (label.indexOf(']') !== 65) return null; if (label.indexOf(']') !== 65) return null;
const hash = `0x${label.slice(1, 65)}`; const hash = `0x${label.slice(1, 65)}`;
if (!isHex(hash)) return null; if (!isHex(hash)) return null;
return hash; return hash;
} }
export function labelhash(label: string) { export function labelhash(label: string) {
if (!label) { if (!label) {
return bytesToHex(new Uint8Array(32).fill(0)); return bytesToHex(new Uint8Array(32).fill(0));
} }
return encodedLabelToLabelhash(label) || keccak256(new TextEncoder().encode(label)); return (
encodedLabelToLabelhash(label) ||
keccak256(new TextEncoder().encode(label))
);
} }
export function makeLabelNodeAndParent(name: string) { export function makeLabelNodeAndParent(name: string) {
const labels = name.split('.'); const labels = name.split('.');
const label = labels.shift() as string; const label = labels.shift() as string;
const parentNode = namehash(labels.join('.')); const parentNode = namehash(labels.join('.'));
return { return {
label, label,
labelhash: labelhash(label), labelhash: labelhash(label),
parentNode, parentNode,
}; };
} }
// https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/contracts/consts.ts // https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/contracts/consts.ts
export const EnsContracts: { export const EnsContracts: {
[key: NetIdType]: { [key: NetIdType]: {
ensRegistry: string; ensRegistry: string;
ensPublicResolver: string; ensPublicResolver: string;
ensNameWrapper: string; ensNameWrapper: string;
}; };
} = { } = {
[NetId.MAINNET]: { [NetId.MAINNET]: {
ensRegistry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', ensRegistry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
ensPublicResolver: '0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63', ensPublicResolver: '0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63',
ensNameWrapper: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401', ensNameWrapper: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401',
}, },
[NetId.SEPOLIA]: { [NetId.SEPOLIA]: {
ensRegistry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', ensRegistry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
ensPublicResolver: '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD', ensPublicResolver: '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD',
ensNameWrapper: '0x0635513f179D50A207757E05759CbD106d7dFcE8', ensNameWrapper: '0x0635513f179D50A207757E05759CbD106d7dFcE8',
}, },
}; };
/** /**
* ENSUtils to manage on-chain registered relayers * ENSUtils to manage on-chain registered relayers
*/ */
export class ENSUtils { export class ENSUtils {
ENSRegistry?: ENSRegistry; ENSRegistry?: ENSRegistry;
ENSResolver?: ENSResolver; ENSResolver?: ENSResolver;
ENSNameWrapper?: ENSNameWrapper; ENSNameWrapper?: ENSNameWrapper;
provider: AbstractProvider; provider: AbstractProvider;
constructor(provider: AbstractProvider) { constructor(provider: AbstractProvider) {
this.provider = provider; this.provider = provider;
}
async getContracts() {
const { chainId } = await this.provider.getNetwork();
const { ensRegistry, ensPublicResolver, ensNameWrapper } = EnsContracts[Number(chainId)];
this.ENSRegistry = ENSRegistry__factory.connect(ensRegistry, this.provider);
this.ENSResolver = ENSResolver__factory.connect(ensPublicResolver, this.provider);
this.ENSNameWrapper = ENSNameWrapper__factory.connect(ensNameWrapper, this.provider);
}
async getOwner(name: string) {
if (!this.ENSRegistry) {
await this.getContracts();
} }
return (this.ENSRegistry as ENSRegistry).owner(namehash(name)); async getContracts() {
} const { chainId } = await this.provider.getNetwork();
// nameWrapper connected with wallet signer const { ensRegistry, ensPublicResolver, ensNameWrapper } =
async unwrap(signer: Signer, name: string) { EnsContracts[Number(chainId)];
if (!this.ENSNameWrapper) {
await this.getContracts(); this.ENSRegistry = ENSRegistry__factory.connect(
ensRegistry,
this.provider,
);
this.ENSResolver = ENSResolver__factory.connect(
ensPublicResolver,
this.provider,
);
this.ENSNameWrapper = ENSNameWrapper__factory.connect(
ensNameWrapper,
this.provider,
);
} }
const owner = (signer as unknown as { address: string }).address; async getOwner(name: string) {
if (!this.ENSRegistry) {
await this.getContracts();
}
const nameWrapper = (this.ENSNameWrapper as ENSNameWrapper).connect(signer); return (this.ENSRegistry as ENSRegistry).owner(namehash(name));
const { labelhash } = makeLabelNodeAndParent(name);
return nameWrapper.unwrapETH2LD(labelhash, owner, owner);
}
// https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/functions/wallet/createSubname.ts
async setSubnodeRecord(signer: Signer, name: string) {
if (!this.ENSResolver) {
await this.getContracts();
} }
const resolver = this.ENSResolver as ENSResolver; // nameWrapper connected with wallet signer
const registry = (this.ENSRegistry as ENSRegistry).connect(signer); async unwrap(signer: Signer, name: string) {
if (!this.ENSNameWrapper) {
await this.getContracts();
}
const owner = (signer as unknown as { address: string }).address; const owner = (signer as unknown as { address: string }).address;
const { labelhash, parentNode } = makeLabelNodeAndParent(name); const nameWrapper = (this.ENSNameWrapper as ENSNameWrapper).connect(
signer,
);
return registry.setSubnodeRecord(parentNode, labelhash, owner, resolver.target, BigInt(0)); const { labelhash } = makeLabelNodeAndParent(name);
}
// https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/functions/wallet/setTextRecord.ts return nameWrapper.unwrapETH2LD(labelhash, owner, owner);
async setText(signer: Signer, name: string, key: string, value: string) {
const resolver = ENSResolver__factory.connect((await this.getResolver(name))?.address as string, signer);
return resolver.setText(namehash(name), key, value);
}
getResolver(name: string) {
return EnsResolver.fromName(this.provider, name);
}
async getText(name: string, key: string) {
const resolver = await this.getResolver(name);
// Retuns null if the name doesn't exist
if (!resolver) {
return resolver;
} }
return (await resolver.getText(key)) || ''; // https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/functions/wallet/createSubname.ts
} async setSubnodeRecord(signer: Signer, name: string) {
if (!this.ENSResolver) {
await this.getContracts();
}
const resolver = this.ENSResolver as ENSResolver;
const registry = (this.ENSRegistry as ENSRegistry).connect(signer);
const owner = (signer as unknown as { address: string }).address;
const { labelhash, parentNode } = makeLabelNodeAndParent(name);
return registry.setSubnodeRecord(
parentNode,
labelhash,
owner,
resolver.target,
BigInt(0),
);
}
// https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/functions/wallet/setTextRecord.ts
async setText(signer: Signer, name: string, key: string, value: string) {
const resolver = ENSResolver__factory.connect(
(await this.getResolver(name))?.address as string,
signer,
);
return resolver.setText(namehash(name), key, value);
}
getResolver(name: string) {
return EnsResolver.fromName(this.provider, name);
}
async getText(name: string, key: string) {
const resolver = await this.getResolver(name);
// Retuns null if the name doesn't exist
if (!resolver) {
return resolver;
}
return (await resolver.getText(key)) || '';
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,365 +2,379 @@ import { downloadZip } from '../zip';
import { IndexedDB } from '../idb'; import { IndexedDB } from '../idb';
import { import {
BaseTornadoService, BaseTornadoService,
BaseTornadoServiceConstructor, BaseTornadoServiceConstructor,
BaseEchoService, BaseEchoService,
BaseEchoServiceConstructor, BaseEchoServiceConstructor,
BaseEncryptedNotesService, BaseEncryptedNotesService,
BaseEncryptedNotesServiceConstructor, BaseEncryptedNotesServiceConstructor,
BaseGovernanceService, BaseGovernanceService,
BaseGovernanceServiceConstructor, BaseGovernanceServiceConstructor,
BaseRegistryService, BaseRegistryService,
BaseRegistryServiceConstructor, BaseRegistryServiceConstructor,
} from './base'; } from './base';
import { import {
BaseEvents, BaseEvents,
MinimalEvents, MinimalEvents,
DepositsEvents, DepositsEvents,
WithdrawalsEvents, WithdrawalsEvents,
CachedEvents, CachedEvents,
EchoEvents, EchoEvents,
EncryptedNotesEvents, EncryptedNotesEvents,
AllGovernanceEvents, AllGovernanceEvents,
RegistersEvents, RegistersEvents,
} from './types'; } from './types';
export async function saveDBEvents<T extends MinimalEvents>({ export async function saveDBEvents<T extends MinimalEvents>({
idb, idb,
instanceName, instanceName,
events, events,
lastBlock, lastBlock,
}: { }: {
idb: IndexedDB; idb: IndexedDB;
instanceName: string; instanceName: string;
events: T[]; events: T[];
lastBlock: number; lastBlock: number;
}) { }) {
try { try {
const formattedEvents = events.map((e) => { const formattedEvents = events.map((e) => {
return { return {
eid: `${e.transactionHash}_${e.logIndex}`, eid: `${e.transactionHash}_${e.logIndex}`,
...e, ...e,
}; };
}); });
await idb.createMultipleTransactions({ await idb.createMultipleTransactions({
data: formattedEvents, data: formattedEvents,
storeName: instanceName, storeName: instanceName,
}); });
await idb.putItem({ await idb.putItem({
data: { data: {
blockNumber: lastBlock, blockNumber: lastBlock,
name: instanceName, name: instanceName,
}, },
storeName: 'lastEvents', storeName: 'lastEvents',
}); });
} catch (err) { } catch (err) {
console.log('Method saveDBEvents has error'); console.log('Method saveDBEvents has error');
console.log(err); console.log(err);
} }
} }
export async function loadDBEvents<T extends MinimalEvents>({ export async function loadDBEvents<T extends MinimalEvents>({
idb, idb,
instanceName, instanceName,
}: { }: {
idb: IndexedDB; idb: IndexedDB;
instanceName: string; instanceName: string;
}): Promise<BaseEvents<T>> { }): Promise<BaseEvents<T>> {
try { try {
const lastBlockStore = await idb.getItem<{ blockNumber: number; name: string }>({ const lastBlockStore = await idb.getItem<{
storeName: 'lastEvents', blockNumber: number;
key: instanceName, name: string;
}); }>({
storeName: 'lastEvents',
key: instanceName,
});
if (!lastBlockStore?.blockNumber) { if (!lastBlockStore?.blockNumber) {
return { return {
events: [], events: [],
lastBlock: 0, lastBlock: 0,
}; };
}
const events = (
await idb.getAll<(T & { eid?: string })[]>({
storeName: instanceName,
})
).map((e) => {
delete e.eid;
return e;
});
return {
events,
lastBlock: lastBlockStore.blockNumber,
};
} catch (err) {
console.log('Method loadDBEvents has error');
console.log(err);
return {
events: [],
lastBlock: 0,
};
} }
const events = (await idb.getAll<(T & { eid?: string })[]>({ storeName: instanceName })).map((e) => {
delete e.eid;
return e;
});
return {
events,
lastBlock: lastBlockStore.blockNumber,
};
} catch (err) {
console.log('Method loadDBEvents has error');
console.log(err);
return {
events: [],
lastBlock: 0,
};
}
} }
export async function loadRemoteEvents<T extends MinimalEvents>({ export async function loadRemoteEvents<T extends MinimalEvents>({
staticUrl, staticUrl,
instanceName, instanceName,
deployedBlock, deployedBlock,
zipDigest, zipDigest,
}: { }: {
staticUrl: string; staticUrl: string;
instanceName: string; instanceName: string;
deployedBlock: number; deployedBlock: number;
zipDigest?: string; zipDigest?: string;
}): Promise<CachedEvents<T>> { }): Promise<CachedEvents<T>> {
try { try {
const zipName = `${instanceName}.json`.toLowerCase(); const zipName = `${instanceName}.json`.toLowerCase();
const events = await downloadZip<T[]>({ const events = await downloadZip<T[]>({
staticUrl, staticUrl,
zipName, zipName,
zipDigest, zipDigest,
}); });
if (!Array.isArray(events)) { if (!Array.isArray(events)) {
const errStr = `Invalid events from ${staticUrl}/${zipName}`; const errStr = `Invalid events from ${staticUrl}/${zipName}`;
throw new Error(errStr); throw new Error(errStr);
}
return {
events,
lastBlock: events[events.length - 1]?.blockNumber || deployedBlock,
fromCache: true,
};
} catch (err) {
console.log('Method loadRemoteEvents has error');
console.log(err);
return {
events: [],
lastBlock: deployedBlock,
fromCache: true,
};
} }
return {
events,
lastBlock: events[events.length - 1]?.blockNumber || deployedBlock,
fromCache: true,
};
} catch (err) {
console.log('Method loadRemoteEvents has error');
console.log(err);
return {
events: [],
lastBlock: deployedBlock,
fromCache: true,
};
}
} }
export interface DBTornadoServiceConstructor extends BaseTornadoServiceConstructor { export interface DBTornadoServiceConstructor
staticUrl: string; extends BaseTornadoServiceConstructor {
idb: IndexedDB; staticUrl: string;
idb: IndexedDB;
} }
export class DBTornadoService extends BaseTornadoService { export class DBTornadoService extends BaseTornadoService {
staticUrl: string; staticUrl: string;
idb: IndexedDB; idb: IndexedDB;
zipDigest?: string; zipDigest?: string;
constructor(params: DBTornadoServiceConstructor) { constructor(params: DBTornadoServiceConstructor) {
super(params); super(params);
this.staticUrl = params.staticUrl; this.staticUrl = params.staticUrl;
this.idb = params.idb; this.idb = params.idb;
} }
async getEventsFromDB() { async getEventsFromDB() {
return await loadDBEvents<DepositsEvents | WithdrawalsEvents>({ return await loadDBEvents<DepositsEvents | WithdrawalsEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
}); });
} }
async getEventsFromCache() { async getEventsFromCache() {
return await loadRemoteEvents<DepositsEvents | WithdrawalsEvents>({ return await loadRemoteEvents<DepositsEvents | WithdrawalsEvents>({
staticUrl: this.staticUrl, staticUrl: this.staticUrl,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
deployedBlock: this.deployedBlock, deployedBlock: this.deployedBlock,
zipDigest: this.zipDigest, zipDigest: this.zipDigest,
}); });
} }
async saveEvents({ events, lastBlock }: BaseEvents<DepositsEvents | WithdrawalsEvents>) { async saveEvents({
await saveDBEvents<DepositsEvents | WithdrawalsEvents>({ events,
idb: this.idb, lastBlock,
instanceName: this.getInstanceName(), }: BaseEvents<DepositsEvents | WithdrawalsEvents>) {
events, await saveDBEvents<DepositsEvents | WithdrawalsEvents>({
lastBlock, idb: this.idb,
}); instanceName: this.getInstanceName(),
} events,
lastBlock,
});
}
} }
export interface DBEchoServiceConstructor extends BaseEchoServiceConstructor { export interface DBEchoServiceConstructor extends BaseEchoServiceConstructor {
staticUrl: string; staticUrl: string;
idb: IndexedDB; idb: IndexedDB;
} }
export class DBEchoService extends BaseEchoService { export class DBEchoService extends BaseEchoService {
staticUrl: string; staticUrl: string;
idb: IndexedDB; idb: IndexedDB;
zipDigest?: string; zipDigest?: string;
constructor(params: DBEchoServiceConstructor) { constructor(params: DBEchoServiceConstructor) {
super(params); super(params);
this.staticUrl = params.staticUrl; this.staticUrl = params.staticUrl;
this.idb = params.idb; this.idb = params.idb;
} }
async getEventsFromDB() { async getEventsFromDB() {
return await loadDBEvents<EchoEvents>({ return await loadDBEvents<EchoEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
}); });
} }
async getEventsFromCache() { async getEventsFromCache() {
return await loadRemoteEvents<EchoEvents>({ return await loadRemoteEvents<EchoEvents>({
staticUrl: this.staticUrl, staticUrl: this.staticUrl,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
deployedBlock: this.deployedBlock, deployedBlock: this.deployedBlock,
zipDigest: this.zipDigest, zipDigest: this.zipDigest,
}); });
} }
async saveEvents({ events, lastBlock }: BaseEvents<EchoEvents>) { async saveEvents({ events, lastBlock }: BaseEvents<EchoEvents>) {
await saveDBEvents<EchoEvents>({ await saveDBEvents<EchoEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
events, events,
lastBlock, lastBlock,
}); });
} }
} }
export interface DBEncryptedNotesServiceConstructor extends BaseEncryptedNotesServiceConstructor { export interface DBEncryptedNotesServiceConstructor
staticUrl: string; extends BaseEncryptedNotesServiceConstructor {
idb: IndexedDB; staticUrl: string;
idb: IndexedDB;
} }
export class DBEncryptedNotesService extends BaseEncryptedNotesService { export class DBEncryptedNotesService extends BaseEncryptedNotesService {
staticUrl: string; staticUrl: string;
idb: IndexedDB; idb: IndexedDB;
zipDigest?: string; zipDigest?: string;
constructor(params: DBEncryptedNotesServiceConstructor) { constructor(params: DBEncryptedNotesServiceConstructor) {
super(params); super(params);
this.staticUrl = params.staticUrl; this.staticUrl = params.staticUrl;
this.idb = params.idb; this.idb = params.idb;
} }
async getEventsFromDB() { async getEventsFromDB() {
return await loadDBEvents<EncryptedNotesEvents>({ return await loadDBEvents<EncryptedNotesEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
}); });
} }
async getEventsFromCache() { async getEventsFromCache() {
return await loadRemoteEvents<EncryptedNotesEvents>({ return await loadRemoteEvents<EncryptedNotesEvents>({
staticUrl: this.staticUrl, staticUrl: this.staticUrl,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
deployedBlock: this.deployedBlock, deployedBlock: this.deployedBlock,
zipDigest: this.zipDigest, zipDigest: this.zipDigest,
}); });
} }
async saveEvents({ events, lastBlock }: BaseEvents<EncryptedNotesEvents>) { async saveEvents({ events, lastBlock }: BaseEvents<EncryptedNotesEvents>) {
await saveDBEvents<EncryptedNotesEvents>({ await saveDBEvents<EncryptedNotesEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
events, events,
lastBlock, lastBlock,
}); });
} }
} }
export interface DBGovernanceServiceConstructor extends BaseGovernanceServiceConstructor { export interface DBGovernanceServiceConstructor
staticUrl: string; extends BaseGovernanceServiceConstructor {
idb: IndexedDB; staticUrl: string;
idb: IndexedDB;
} }
export class DBGovernanceService extends BaseGovernanceService { export class DBGovernanceService extends BaseGovernanceService {
staticUrl: string; staticUrl: string;
idb: IndexedDB; idb: IndexedDB;
zipDigest?: string; zipDigest?: string;
constructor(params: DBGovernanceServiceConstructor) { constructor(params: DBGovernanceServiceConstructor) {
super(params); super(params);
this.staticUrl = params.staticUrl; this.staticUrl = params.staticUrl;
this.idb = params.idb; this.idb = params.idb;
} }
async getEventsFromDB() { async getEventsFromDB() {
return await loadDBEvents<AllGovernanceEvents>({ return await loadDBEvents<AllGovernanceEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
}); });
} }
async getEventsFromCache() { async getEventsFromCache() {
return await loadRemoteEvents<AllGovernanceEvents>({ return await loadRemoteEvents<AllGovernanceEvents>({
staticUrl: this.staticUrl, staticUrl: this.staticUrl,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
deployedBlock: this.deployedBlock, deployedBlock: this.deployedBlock,
zipDigest: this.zipDigest, zipDigest: this.zipDigest,
}); });
} }
async saveEvents({ events, lastBlock }: BaseEvents<AllGovernanceEvents>) { async saveEvents({ events, lastBlock }: BaseEvents<AllGovernanceEvents>) {
await saveDBEvents<AllGovernanceEvents>({ await saveDBEvents<AllGovernanceEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
events, events,
lastBlock, lastBlock,
}); });
} }
} }
export interface DBRegistryServiceConstructor extends BaseRegistryServiceConstructor { export interface DBRegistryServiceConstructor
staticUrl: string; extends BaseRegistryServiceConstructor {
idb: IndexedDB; staticUrl: string;
idb: IndexedDB;
} }
export class DBRegistryService extends BaseRegistryService { export class DBRegistryService extends BaseRegistryService {
staticUrl: string; staticUrl: string;
idb: IndexedDB; idb: IndexedDB;
zipDigest?: string; zipDigest?: string;
constructor(params: DBRegistryServiceConstructor) { constructor(params: DBRegistryServiceConstructor) {
super(params); super(params);
this.staticUrl = params.staticUrl; this.staticUrl = params.staticUrl;
this.idb = params.idb; this.idb = params.idb;
} }
async getEventsFromDB() { async getEventsFromDB() {
return await loadDBEvents<RegistersEvents>({ return await loadDBEvents<RegistersEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
}); });
} }
async getEventsFromCache() { async getEventsFromCache() {
return await loadRemoteEvents<RegistersEvents>({ return await loadRemoteEvents<RegistersEvents>({
staticUrl: this.staticUrl, staticUrl: this.staticUrl,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
deployedBlock: this.deployedBlock, deployedBlock: this.deployedBlock,
zipDigest: this.zipDigest, zipDigest: this.zipDigest,
}); });
} }
async saveEvents({ events, lastBlock }: BaseEvents<RegistersEvents>) { async saveEvents({ events, lastBlock }: BaseEvents<RegistersEvents>) {
await saveDBEvents<RegistersEvents>({ await saveDBEvents<RegistersEvents>({
idb: this.idb, idb: this.idb,
instanceName: this.getInstanceName(), instanceName: this.getInstanceName(),
events, events,
lastBlock, lastBlock,
}); });
} }
} }

View File

@@ -1,84 +1,84 @@
import { RelayerParams } from '../relayerClient'; import { RelayerParams } from '../relayerClient';
export interface BaseEvents<T> { export interface BaseEvents<T> {
events: T[]; events: T[];
lastBlock: number; lastBlock: number;
} }
export interface CachedEvents<T> extends BaseEvents<T> { export interface CachedEvents<T> extends BaseEvents<T> {
fromCache: boolean; fromCache: boolean;
} }
export interface BaseGraphEvents<T> { export interface BaseGraphEvents<T> {
events: T[]; events: T[];
lastSyncBlock: number; lastSyncBlock: number;
} }
export interface MinimalEvents { export interface MinimalEvents {
blockNumber: number; blockNumber: number;
logIndex: number; logIndex: number;
transactionHash: string; transactionHash: string;
} }
export interface GovernanceEvents extends MinimalEvents { export interface GovernanceEvents extends MinimalEvents {
event: string; event: string;
} }
export interface GovernanceProposalCreatedEvents extends GovernanceEvents { export interface GovernanceProposalCreatedEvents extends GovernanceEvents {
id: number; id: number;
proposer: string; proposer: string;
target: string; target: string;
startTime: number; startTime: number;
endTime: number; endTime: number;
description: string; description: string;
} }
export interface GovernanceVotedEvents extends GovernanceEvents { export interface GovernanceVotedEvents extends GovernanceEvents {
proposalId: number; proposalId: number;
voter: string; voter: string;
support: boolean; support: boolean;
votes: string; votes: string;
from: string; from: string;
input: string; input: string;
} }
export interface GovernanceDelegatedEvents extends GovernanceEvents { export interface GovernanceDelegatedEvents extends GovernanceEvents {
account: string; account: string;
delegateTo: string; delegateTo: string;
} }
export interface GovernanceUndelegatedEvents extends GovernanceEvents { export interface GovernanceUndelegatedEvents extends GovernanceEvents {
account: string; account: string;
delegateFrom: string; delegateFrom: string;
} }
export type AllGovernanceEvents = export type AllGovernanceEvents =
| GovernanceProposalCreatedEvents | GovernanceProposalCreatedEvents
| GovernanceVotedEvents | GovernanceVotedEvents
| GovernanceDelegatedEvents | GovernanceDelegatedEvents
| GovernanceUndelegatedEvents; | GovernanceUndelegatedEvents;
export type RegistersEvents = MinimalEvents & RelayerParams; export type RegistersEvents = MinimalEvents & RelayerParams;
export interface DepositsEvents extends MinimalEvents { export interface DepositsEvents extends MinimalEvents {
commitment: string; commitment: string;
leafIndex: number; leafIndex: number;
timestamp: number; timestamp: number;
from: string; from: string;
} }
export interface WithdrawalsEvents extends MinimalEvents { export interface WithdrawalsEvents extends MinimalEvents {
nullifierHash: string; nullifierHash: string;
to: string; to: string;
fee: string; fee: string;
timestamp: number; timestamp: number;
} }
export interface EchoEvents extends MinimalEvents { export interface EchoEvents extends MinimalEvents {
address: string; address: string;
encryptedAccount: string; encryptedAccount: string;
} }
export interface EncryptedNotesEvents extends MinimalEvents { export interface EncryptedNotesEvents extends MinimalEvents {
encryptedNote: string; encryptedNote: string;
} }

View File

@@ -7,7 +7,7 @@ const DUMMY_ADDRESS = '0x1111111111111111111111111111111111111111';
const DUMMY_NONCE = 1024; const DUMMY_NONCE = 1024;
const DUMMY_WITHDRAW_DATA = const DUMMY_WITHDRAW_DATA =
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'; '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111';
/** /**
* Example: * Example:
@@ -15,141 +15,179 @@ const DUMMY_WITHDRAW_DATA =
* amountInWei (0.1 ETH) * tokenDecimals (18) * tokenPriceInWei (0.0008) = 125 TOKEN * amountInWei (0.1 ETH) * tokenDecimals (18) * tokenPriceInWei (0.0008) = 125 TOKEN
*/ */
export function convertETHToTokenAmount( export function convertETHToTokenAmount(
amountInWei: BigNumberish, amountInWei: BigNumberish,
tokenPriceInWei: BigNumberish, tokenPriceInWei: BigNumberish,
tokenDecimals: number = 18, tokenDecimals: number = 18,
): bigint { ): bigint {
const tokenDecimalsMultiplier = BigInt(10 ** Number(tokenDecimals)); const tokenDecimalsMultiplier = BigInt(10 ** Number(tokenDecimals));
return (BigInt(amountInWei) * tokenDecimalsMultiplier) / BigInt(tokenPriceInWei); return (
(BigInt(amountInWei) * tokenDecimalsMultiplier) /
BigInt(tokenPriceInWei)
);
} }
export interface RelayerFeeParams { export interface RelayerFeeParams {
gasPrice: BigNumberish; gasPrice: BigNumberish;
gasLimit?: BigNumberish; gasLimit?: BigNumberish;
l1Fee?: BigNumberish; l1Fee?: BigNumberish;
denomination: BigNumberish; denomination: BigNumberish;
ethRefund: BigNumberish; ethRefund: BigNumberish;
tokenPriceInWei: BigNumberish; tokenPriceInWei: BigNumberish;
tokenDecimals: number; tokenDecimals: number;
relayerFeePercent?: number; relayerFeePercent?: number;
isEth?: boolean; isEth?: boolean;
premiumPercent?: number; premiumPercent?: number;
} }
export class TornadoFeeOracle { export class TornadoFeeOracle {
provider: JsonRpcApiProvider; provider: JsonRpcApiProvider;
ovmGasPriceOracle?: OvmGasPriceOracle; ovmGasPriceOracle?: OvmGasPriceOracle;
constructor(provider: JsonRpcApiProvider, ovmGasPriceOracle?: OvmGasPriceOracle) { constructor(
this.provider = provider; provider: JsonRpcApiProvider,
ovmGasPriceOracle?: OvmGasPriceOracle,
) {
this.provider = provider;
if (ovmGasPriceOracle) { if (ovmGasPriceOracle) {
this.ovmGasPriceOracle = ovmGasPriceOracle; this.ovmGasPriceOracle = ovmGasPriceOracle;
}
}
/**
* Calculates Gas Price
* We apply 50% premium of EIP-1559 network fees instead of 100% from ethers.js
* (This should cover up to 4 full blocks which is equivalent of minute)
* (A single block can bump 12.5% of fees, see the methodology https://hackmd.io/@tvanepps/1559-wallets)
* (Still it is recommended to use 100% premium for sending transactions to prevent stucking it)
*/
async gasPrice() {
const [block, getGasPrice, getPriorityFee] = await Promise.all([
this.provider.getBlock('latest'),
(async () => {
try {
return BigInt(await this.provider.send('eth_gasPrice', []));
} catch {
return parseUnits('1', 'gwei');
} }
})(), }
(async () => {
try { /**
return BigInt(await this.provider.send('eth_maxPriorityFeePerGas', [])); * Calculates Gas Price
} catch { * We apply 50% premium of EIP-1559 network fees instead of 100% from ethers.js
return BigInt(0); * (This should cover up to 4 full blocks which is equivalent of minute)
* (A single block can bump 12.5% of fees, see the methodology https://hackmd.io/@tvanepps/1559-wallets)
* (Still it is recommended to use 100% premium for sending transactions to prevent stucking it)
*/
async gasPrice() {
const [block, getGasPrice, getPriorityFee] = await Promise.all([
this.provider.getBlock('latest'),
(async () => {
try {
return BigInt(await this.provider.send('eth_gasPrice', []));
} catch {
return parseUnits('1', 'gwei');
}
})(),
(async () => {
try {
return BigInt(
await this.provider.send(
'eth_maxPriorityFeePerGas',
[],
),
);
} catch {
return BigInt(0);
}
})(),
]);
return block?.baseFeePerGas
? (block.baseFeePerGas * BigInt(15)) / BigInt(10) + getPriorityFee
: getGasPrice;
}
/**
* Calculate L1 fee for op-stack chains
*
* This is required since relayers would pay the full transaction fees for users
*/
fetchL1OptimismFee(tx?: TransactionLike): Promise<bigint> {
if (!this.ovmGasPriceOracle) {
return new Promise((resolve) => resolve(BigInt(0)));
} }
})(),
]);
return block?.baseFeePerGas ? (block.baseFeePerGas * BigInt(15)) / BigInt(10) + getPriorityFee : getGasPrice; if (!tx) {
} // this tx is only used to simulate bytes size of the encoded tx so has nothing to with the accuracy
// inspired by the old style classic-ui calculation
tx = {
type: 0,
gasLimit: 1_000_000,
nonce: DUMMY_NONCE,
data: DUMMY_WITHDRAW_DATA,
gasPrice: parseUnits('1', 'gwei'),
to: DUMMY_ADDRESS,
};
}
/** return this.ovmGasPriceOracle.getL1Fee.staticCall(
* Calculate L1 fee for op-stack chains Transaction.from(tx).unsignedSerialized,
* );
* This is required since relayers would pay the full transaction fees for users
*/
fetchL1OptimismFee(tx?: TransactionLike): Promise<bigint> {
if (!this.ovmGasPriceOracle) {
return new Promise((resolve) => resolve(BigInt(0)));
} }
if (!tx) { /**
// this tx is only used to simulate bytes size of the encoded tx so has nothing to with the accuracy * We don't need to distinguish default refunds by tokens since most users interact with other defi protocols after withdrawal
// inspired by the old style classic-ui calculation * So we default with 1M gas which is enough for two or three swaps
tx = { * Using 30 gwei for default but it is recommended to supply cached gasPrice value from the UI
type: 0, */
gasLimit: 1_000_000, defaultEthRefund(gasPrice?: BigNumberish, gasLimit?: BigNumberish): bigint {
nonce: DUMMY_NONCE, return (
data: DUMMY_WITHDRAW_DATA, (gasPrice ? BigInt(gasPrice) : parseUnits('30', 'gwei')) *
gasPrice: parseUnits('1', 'gwei'), BigInt(gasLimit || 1_000_000)
to: DUMMY_ADDRESS, );
};
} }
return this.ovmGasPriceOracle.getL1Fee.staticCall(Transaction.from(tx).unsignedSerialized); /**
} * Calculates token amount for required ethRefund purchases required to calculate fees
*/
/** calculateTokenAmount(
* We don't need to distinguish default refunds by tokens since most users interact with other defi protocols after withdrawal ethRefund: BigNumberish,
* So we default with 1M gas which is enough for two or three swaps tokenPriceInEth: BigNumberish,
* Using 30 gwei for default but it is recommended to supply cached gasPrice value from the UI tokenDecimals?: number,
*/ ): bigint {
defaultEthRefund(gasPrice?: BigNumberish, gasLimit?: BigNumberish): bigint { return convertETHToTokenAmount(
return (gasPrice ? BigInt(gasPrice) : parseUnits('30', 'gwei')) * BigInt(gasLimit || 1_000_000); ethRefund,
} tokenPriceInEth,
tokenDecimals,
/** );
* Calculates token amount for required ethRefund purchases required to calculate fees
*/
calculateTokenAmount(ethRefund: BigNumberish, tokenPriceInEth: BigNumberish, tokenDecimals?: number): bigint {
return convertETHToTokenAmount(ethRefund, tokenPriceInEth, tokenDecimals);
}
/**
* Warning: For tokens you need to check if the fees are above denomination
* (Usually happens for small denomination pool or if the gas price is high)
*/
calculateRelayerFee({
gasPrice,
gasLimit = 600_000,
l1Fee = 0,
denomination,
ethRefund = BigInt(0),
tokenPriceInWei,
tokenDecimals = 18,
relayerFeePercent = 0.33,
isEth = true,
premiumPercent = 20,
}: RelayerFeeParams): bigint {
const gasCosts = BigInt(gasPrice) * BigInt(gasLimit) + BigInt(l1Fee);
const relayerFee = (BigInt(denomination) * BigInt(Math.floor(10000 * relayerFeePercent))) / BigInt(10000 * 100);
if (isEth) {
// Add 20% premium
return ((gasCosts + relayerFee) * BigInt(premiumPercent ? 100 + premiumPercent : 100)) / BigInt(100);
} }
const feeInEth = gasCosts + BigInt(ethRefund); /**
* Warning: For tokens you need to check if the fees are above denomination
* (Usually happens for small denomination pool or if the gas price is high)
*/
calculateRelayerFee({
gasPrice,
gasLimit = 600_000,
l1Fee = 0,
denomination,
ethRefund = BigInt(0),
tokenPriceInWei,
tokenDecimals = 18,
relayerFeePercent = 0.33,
isEth = true,
premiumPercent = 20,
}: RelayerFeeParams): bigint {
const gasCosts = BigInt(gasPrice) * BigInt(gasLimit) + BigInt(l1Fee);
return ( const relayerFee =
((convertETHToTokenAmount(feeInEth, tokenPriceInWei, tokenDecimals) + relayerFee) * (BigInt(denomination) *
BigInt(premiumPercent ? 100 + premiumPercent : 100)) / BigInt(Math.floor(10000 * relayerFeePercent))) /
BigInt(100) BigInt(10000 * 100);
);
} if (isEth) {
// Add 20% premium
return (
((gasCosts + relayerFee) *
BigInt(premiumPercent ? 100 + premiumPercent : 100)) /
BigInt(100)
);
}
const feeInEth = gasCosts + BigInt(ethRefund);
return (
((convertETHToTokenAmount(
feeInEth,
tokenPriceInWei,
tokenDecimals,
) +
relayerFee) *
BigInt(premiumPercent ? 100 + premiumPercent : 100)) /
BigInt(100)
);
}
} }

View File

@@ -3,52 +3,52 @@ import { NetId, NetIdType } from './networkConfig';
// https://dev.gas.zip/gas/chain-support/inbound // https://dev.gas.zip/gas/chain-support/inbound
export const gasZipInbounds: { [key in NetIdType]: string } = { export const gasZipInbounds: { [key in NetIdType]: string } = {
[NetId.MAINNET]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.MAINNET]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
[NetId.BSC]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.BSC]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
[NetId.POLYGON]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.POLYGON]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
[NetId.OPTIMISM]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.OPTIMISM]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
[NetId.ARBITRUM]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.ARBITRUM]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
[NetId.GNOSIS]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.GNOSIS]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
[NetId.AVALANCHE]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604', [NetId.AVALANCHE]: '0x391E7C679d29bD940d63be94AD22A25d25b5A604',
}; };
// https://dev.gas.zip/gas/chain-support/outbound // https://dev.gas.zip/gas/chain-support/outbound
export const gasZipID: { [key in NetIdType]: number } = { export const gasZipID: { [key in NetIdType]: number } = {
[NetId.MAINNET]: 255, [NetId.MAINNET]: 255,
[NetId.BSC]: 14, [NetId.BSC]: 14,
[NetId.POLYGON]: 17, [NetId.POLYGON]: 17,
[NetId.OPTIMISM]: 55, [NetId.OPTIMISM]: 55,
[NetId.ARBITRUM]: 57, [NetId.ARBITRUM]: 57,
[NetId.GNOSIS]: 16, [NetId.GNOSIS]: 16,
[NetId.AVALANCHE]: 15, [NetId.AVALANCHE]: 15,
[NetId.SEPOLIA]: 102, [NetId.SEPOLIA]: 102,
}; };
// https://dev.gas.zip/gas/code-examples/eoaDeposit // https://dev.gas.zip/gas/code-examples/eoaDeposit
export function gasZipInput(to: string, shorts: number[]): string | null { export function gasZipInput(to: string, shorts: number[]): string | null {
let data = '0x'; let data = '0x';
if (isAddress(to)) { if (isAddress(to)) {
if (to.length === 42) { if (to.length === 42) {
data += '02'; data += '02';
data += to.slice(2); data += to.slice(2);
} else {
return null;
}
} else { } else {
return null; data += '01'; // to == sender
} }
} else {
data += '01'; // to == sender
}
for (const i in shorts) { for (const i in shorts) {
data += Number(shorts[i]).toString(16).padStart(4, '0'); data += Number(shorts[i]).toString(16).padStart(4, '0');
} }
return data; return data;
} }
export function gasZipMinMax(ethUsd: number) { export function gasZipMinMax(ethUsd: number) {
return { return {
min: 1 / ethUsd, min: 1 / ethUsd,
max: 50 / ethUsd, max: 50 / ethUsd,
ethUsd, ethUsd,
}; };
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -2,450 +2,498 @@
import { openDB, deleteDB, OpenDBCallbacks, IDBPDatabase } from 'idb'; import { openDB, deleteDB, OpenDBCallbacks, IDBPDatabase } from 'idb';
import { getConfig, NetIdType } from './networkConfig'; import { getConfig, NetIdType } from './networkConfig';
export const INDEX_DB_ERROR = 'A mutation operation was attempted on a database that did not allow mutations.'; export const INDEX_DB_ERROR =
'A mutation operation was attempted on a database that did not allow mutations.';
export interface IDBIndex { export interface IDBIndex {
name: string; name: string;
unique?: boolean; unique?: boolean;
} }
export interface IDBStores { export interface IDBStores {
name: string; name: string;
keyPath?: string; keyPath?: string;
indexes?: IDBIndex[]; indexes?: IDBIndex[];
} }
export interface IDBConstructor { export interface IDBConstructor {
dbName: string; dbName: string;
stores?: IDBStores[]; stores?: IDBStores[];
} }
export class IndexedDB { export class IndexedDB {
dbExists: boolean; dbExists: boolean;
isBlocked: boolean; isBlocked: boolean;
// todo: TestDBSchema on any // todo: TestDBSchema on any
options: OpenDBCallbacks<any>; options: OpenDBCallbacks<any>;
dbName: string; dbName: string;
dbVersion: number; dbVersion: number;
db?: IDBPDatabase<any>; db?: IDBPDatabase<any>;
constructor({ dbName, stores }: IDBConstructor) { constructor({ dbName, stores }: IDBConstructor) {
this.dbExists = false; this.dbExists = false;
this.isBlocked = false; this.isBlocked = false;
this.options = { this.options = {
upgrade(db) { upgrade(db) {
Object.values(db.objectStoreNames).forEach((value) => { Object.values(db.objectStoreNames).forEach((value) => {
db.deleteObjectStore(value); db.deleteObjectStore(value);
}); });
[{ name: 'keyval' }, ...(stores || [])].forEach(({ name, keyPath, indexes }) => { [{ name: 'keyval' }, ...(stores || [])].forEach(
const store = db.createObjectStore(name, { ({ name, keyPath, indexes }) => {
keyPath, const store = db.createObjectStore(name, {
autoIncrement: true, keyPath,
}); autoIncrement: true,
});
if (Array.isArray(indexes)) { if (Array.isArray(indexes)) {
indexes.forEach(({ name, unique = false }) => { indexes.forEach(({ name, unique = false }) => {
store.createIndex(name, name, { unique }); store.createIndex(name, name, { unique });
});
}
},
);
},
};
this.dbName = dbName;
this.dbVersion = 35;
}
async initDB() {
try {
if (this.dbExists || this.isBlocked) {
return;
}
this.db = await openDB(this.dbName, this.dbVersion, this.options);
this.db.addEventListener('onupgradeneeded', async () => {
await this._removeExist();
}); });
}
});
},
};
this.dbName = dbName; this.dbExists = true;
this.dbVersion = 35; } catch (err: any) {
} // needed for private mode firefox browser
if (err.message.includes(INDEX_DB_ERROR)) {
console.log('This browser does not support IndexedDB!');
this.isBlocked = true;
return;
}
async initDB() { if (err.message.includes('less than the existing version')) {
try { console.log(`Upgrading DB ${this.dbName} to ${this.dbVersion}`);
if (this.dbExists || this.isBlocked) { await this._removeExist();
return; return;
} }
this.db = await openDB(this.dbName, this.dbVersion, this.options); console.error(`Method initDB has error: ${err.message}`);
this.db.addEventListener('onupgradeneeded', async () => { }
await this._removeExist(); }
});
async _removeExist() {
this.dbExists = true; await deleteDB(this.dbName);
} catch (err: any) { this.dbExists = false;
// needed for private mode firefox browser
if (err.message.includes(INDEX_DB_ERROR)) { await this.initDB();
console.log('This browser does not support IndexedDB!'); }
this.isBlocked = true;
return; async getFromIndex<T>({
} storeName,
indexName,
if (err.message.includes('less than the existing version')) { key,
console.log(`Upgrading DB ${this.dbName} to ${this.dbVersion}`); }: {
await this._removeExist(); storeName: string;
return; indexName: string;
} key?: string;
}): Promise<T | undefined> {
console.error(`Method initDB has error: ${err.message}`); await this.initDB();
}
} if (!this.db) {
return;
async _removeExist() { }
await deleteDB(this.dbName);
this.dbExists = false; try {
return (await this.db.getFromIndex(storeName, indexName, key)) as T;
await this.initDB(); } catch (err: any) {
} throw new Error(`Method getFromIndex has error: ${err.message}`);
}
async getFromIndex<T>({ }
storeName,
indexName, async getAllFromIndex<T>({
key, storeName,
}: { indexName,
storeName: string; key,
indexName: string; count,
key?: string; }: {
}): Promise<T | undefined> { storeName: string;
await this.initDB(); indexName: string;
key?: string;
if (!this.db) { count?: number;
return; }): Promise<T> {
} await this.initDB();
try { if (!this.db) {
return (await this.db.getFromIndex(storeName, indexName, key)) as T; return [] as T;
} catch (err: any) { }
throw new Error(`Method getFromIndex has error: ${err.message}`);
} try {
} return (await this.db.getAllFromIndex(
storeName,
async getAllFromIndex<T>({ indexName,
storeName, key,
indexName, count,
key, )) as T;
count, } catch (err: any) {
}: { throw new Error(`Method getAllFromIndex has error: ${err.message}`);
storeName: string; }
indexName: string; }
key?: string;
count?: number; async getItem<T>({
}): Promise<T> { storeName,
await this.initDB(); key,
}: {
if (!this.db) { storeName: string;
return [] as T; key: string;
} }): Promise<T | undefined> {
await this.initDB();
try {
return (await this.db.getAllFromIndex(storeName, indexName, key, count)) as T; if (!this.db) {
} catch (err: any) { return;
throw new Error(`Method getAllFromIndex has error: ${err.message}`); }
}
} try {
const store = this.db.transaction(storeName).objectStore(storeName);
async getItem<T>({ storeName, key }: { storeName: string; key: string }): Promise<T | undefined> {
await this.initDB(); return (await store.get(key)) as T;
} catch (err: any) {
if (!this.db) { throw new Error(`Method getItem has error: ${err.message}`);
return; }
} }
try { async addItem({
const store = this.db.transaction(storeName).objectStore(storeName); storeName,
data,
return (await store.get(key)) as T; key = '',
} catch (err: any) { }: {
throw new Error(`Method getItem has error: ${err.message}`); storeName: string;
} data: any;
} key: string;
}) {
async addItem({ storeName, data, key = '' }: { storeName: string; data: any; key: string }) { await this.initDB();
await this.initDB();
if (!this.db) {
if (!this.db) { return;
return; }
}
try {
try { const tx = this.db.transaction(storeName, 'readwrite');
const tx = this.db.transaction(storeName, 'readwrite'); const isExist = await tx.objectStore(storeName).get(key);
const isExist = await tx.objectStore(storeName).get(key);
if (!isExist) {
if (!isExist) { await tx.objectStore(storeName).add(data);
await tx.objectStore(storeName).add(data); }
} } catch (err: any) {
} catch (err: any) { throw new Error(`Method addItem has error: ${err.message}`);
throw new Error(`Method addItem has error: ${err.message}`); }
} }
}
async putItem({
async putItem({ storeName, data, key }: { storeName: string; data: any; key?: string }) { storeName,
await this.initDB(); data,
key,
if (!this.db) { }: {
return; storeName: string;
} data: any;
key?: string;
try { }) {
const tx = this.db.transaction(storeName, 'readwrite'); await this.initDB();
await tx.objectStore(storeName).put(data, key); if (!this.db) {
} catch (err: any) { return;
throw new Error(`Method putItem has error: ${err.message}`); }
}
} try {
const tx = this.db.transaction(storeName, 'readwrite');
async deleteItem({ storeName, key }: { storeName: string; key: string }) {
await this.initDB(); await tx.objectStore(storeName).put(data, key);
} catch (err: any) {
if (!this.db) { throw new Error(`Method putItem has error: ${err.message}`);
return; }
} }
try { async deleteItem({ storeName, key }: { storeName: string; key: string }) {
const tx = this.db.transaction(storeName, 'readwrite'); await this.initDB();
await tx.objectStore(storeName).delete(key); if (!this.db) {
} catch (err: any) { return;
throw new Error(`Method deleteItem has error: ${err.message}`); }
}
} try {
const tx = this.db.transaction(storeName, 'readwrite');
async getAll<T>({ storeName }: { storeName: string }): Promise<T> {
await this.initDB(); await tx.objectStore(storeName).delete(key);
} catch (err: any) {
if (!this.db) { throw new Error(`Method deleteItem has error: ${err.message}`);
return [] as T; }
} }
try { async getAll<T>({ storeName }: { storeName: string }): Promise<T> {
const tx = this.db.transaction(storeName, 'readonly'); await this.initDB();
return (await tx.objectStore(storeName).getAll()) as T; if (!this.db) {
} catch (err: any) { return [] as T;
throw new Error(`Method getAll has error: ${err.message}`); }
}
} try {
const tx = this.db.transaction(storeName, 'readonly');
/**
* Simple key-value store inspired by idb-keyval package return (await tx.objectStore(storeName).getAll()) as T;
*/ } catch (err: any) {
getValue<T>(key: string) { throw new Error(`Method getAll has error: ${err.message}`);
return this.getItem<T>({ storeName: 'keyval', key }); }
} }
setValue(key: string, data: any) { /**
return this.putItem({ storeName: 'keyval', key, data }); * Simple key-value store inspired by idb-keyval package
} */
getValue<T>(key: string) {
delValue(key: string) { return this.getItem<T>({ storeName: 'keyval', key });
return this.deleteItem({ storeName: 'keyval', key }); }
}
setValue(key: string, data: any) {
async clearStore({ storeName, mode = 'readwrite' }: { storeName: string; mode: IDBTransactionMode }) { return this.putItem({ storeName: 'keyval', key, data });
await this.initDB(); }
if (!this.db) { delValue(key: string) {
return; return this.deleteItem({ storeName: 'keyval', key });
} }
try { async clearStore({
const tx = this.db.transaction(storeName, mode); storeName,
mode = 'readwrite',
await (tx.objectStore(storeName).clear as () => Promise<void>)(); }: {
} catch (err: any) { storeName: string;
throw new Error(`Method clearStore has error: ${err.message}`); mode: IDBTransactionMode;
} }) {
} await this.initDB();
async createTransactions({ if (!this.db) {
storeName, return;
data, }
mode = 'readwrite',
}: { try {
storeName: string; const tx = this.db.transaction(storeName, mode);
data: any;
mode: IDBTransactionMode; await (tx.objectStore(storeName).clear as () => Promise<void>)();
}) { } catch (err: any) {
await this.initDB(); throw new Error(`Method clearStore has error: ${err.message}`);
}
if (!this.db) { }
return;
} async createTransactions({
storeName,
try { data,
const tx = this.db.transaction(storeName, mode); mode = 'readwrite',
}: {
await (tx.objectStore(storeName).add as (value: any, key?: any) => Promise<any>)(data); storeName: string;
await tx.done; data: any;
} catch (err: any) { mode: IDBTransactionMode;
throw new Error(`Method createTransactions has error: ${err.message}`); }) {
} await this.initDB();
}
if (!this.db) {
async createMultipleTransactions({ return;
storeName, }
data,
index, try {
mode = 'readwrite', const tx = this.db.transaction(storeName, mode);
}: {
storeName: string; await (
data: any[]; tx.objectStore(storeName).add as (
index?: any; value: any,
mode?: IDBTransactionMode; key?: any,
}) { ) => Promise<any>
await this.initDB(); )(data);
await tx.done;
if (!this.db) { } catch (err: any) {
return; throw new Error(
} `Method createTransactions has error: ${err.message}`,
);
try { }
const tx = this.db.transaction(storeName, mode); }
for (const item of data) { async createMultipleTransactions({
if (item) { storeName,
await (tx.store.put as (value: any, key?: any) => Promise<any>)({ ...item, ...index }); data,
index,
mode = 'readwrite',
}: {
storeName: string;
data: any[];
index?: any;
mode?: IDBTransactionMode;
}) {
await this.initDB();
if (!this.db) {
return;
}
try {
const tx = this.db.transaction(storeName, mode);
for (const item of data) {
if (item) {
await (
tx.store.put as (value: any, key?: any) => Promise<any>
)({ ...item, ...index });
}
}
} catch (err: any) {
throw new Error(
`Method createMultipleTransactions has error: ${err.message}`,
);
} }
}
} catch (err: any) {
throw new Error(`Method createMultipleTransactions has error: ${err.message}`);
} }
}
} }
/** /**
* Should check if DB is initialized well * Should check if DB is initialized well
*/ */
export async function getIndexedDB(netId?: NetIdType) { export async function getIndexedDB(netId?: NetIdType) {
// key-value db for settings // key-value db for settings
if (!netId) { if (!netId) {
const idb = new IndexedDB({ dbName: 'tornado-core' }); const idb = new IndexedDB({ dbName: 'tornado-core' });
await idb.initDB();
return idb;
}
const minimalIndexes = [
{
name: 'blockNumber',
unique: false,
},
{
name: 'transactionHash',
unique: false,
},
];
const defaultState = [
{
name: `echo_${netId}`,
keyPath: 'eid',
indexes: [
...minimalIndexes,
{
name: 'address',
unique: false,
},
],
},
{
name: `encrypted_notes_${netId}`,
keyPath: 'eid',
indexes: minimalIndexes,
},
{
name: 'lastEvents',
keyPath: 'name',
indexes: [
{
name: 'name',
unique: false,
},
],
},
];
const config = getConfig(netId);
const { tokens, nativeCurrency, registryContract, governanceContract } =
config;
const stores = [...defaultState];
if (registryContract) {
stores.push({
name: `registered_${netId}`,
keyPath: 'ensName',
indexes: [
...minimalIndexes,
{
name: 'relayerAddress',
unique: false,
},
],
});
}
if (governanceContract) {
stores.push({
name: `governance_${netId}`,
keyPath: 'eid',
indexes: [
...minimalIndexes,
{
name: 'event',
unique: false,
},
],
});
}
Object.entries(tokens).forEach(([token, { instanceAddress }]) => {
Object.keys(instanceAddress).forEach((amount) => {
if (nativeCurrency === token) {
stores.push(
{
name: `stringify_bloom_${netId}_${token}_${amount}`,
keyPath: 'hashBloom',
indexes: [],
},
{
name: `stringify_tree_${netId}_${token}_${amount}`,
keyPath: 'hashTree',
indexes: [],
},
);
}
stores.push(
{
name: `deposits_${netId}_${token}_${amount}`,
keyPath: 'leafIndex', // the key by which it refers to the object must be in all instances of the storage
indexes: [
...minimalIndexes,
{
name: 'commitment',
unique: true,
},
],
},
{
name: `withdrawals_${netId}_${token}_${amount}`,
keyPath: 'eid',
indexes: [
...minimalIndexes,
{
name: 'nullifierHash',
unique: true,
}, // keys on which the index is created
],
},
);
});
});
const idb = new IndexedDB({
dbName: `tornado_core_${netId}`,
stores,
});
await idb.initDB(); await idb.initDB();
return idb; return idb;
}
const minimalIndexes = [
{
name: 'blockNumber',
unique: false,
},
{
name: 'transactionHash',
unique: false,
},
];
const defaultState = [
{
name: `echo_${netId}`,
keyPath: 'eid',
indexes: [
...minimalIndexes,
{
name: 'address',
unique: false,
},
],
},
{
name: `encrypted_notes_${netId}`,
keyPath: 'eid',
indexes: minimalIndexes,
},
{
name: 'lastEvents',
keyPath: 'name',
indexes: [
{
name: 'name',
unique: false,
},
],
},
];
const config = getConfig(netId);
const { tokens, nativeCurrency, registryContract, governanceContract } = config;
const stores = [...defaultState];
if (registryContract) {
stores.push({
name: `registered_${netId}`,
keyPath: 'ensName',
indexes: [
...minimalIndexes,
{
name: 'relayerAddress',
unique: false,
},
],
});
}
if (governanceContract) {
stores.push({
name: `governance_${netId}`,
keyPath: 'eid',
indexes: [
...minimalIndexes,
{
name: 'event',
unique: false,
},
],
});
}
Object.entries(tokens).forEach(([token, { instanceAddress }]) => {
Object.keys(instanceAddress).forEach((amount) => {
if (nativeCurrency === token) {
stores.push(
{
name: `stringify_bloom_${netId}_${token}_${amount}`,
keyPath: 'hashBloom',
indexes: [],
},
{
name: `stringify_tree_${netId}_${token}_${amount}`,
keyPath: 'hashTree',
indexes: [],
},
);
}
stores.push(
{
name: `deposits_${netId}_${token}_${amount}`,
keyPath: 'leafIndex', // the key by which it refers to the object must be in all instances of the storage
indexes: [
...minimalIndexes,
{
name: 'commitment',
unique: true,
},
],
},
{
name: `withdrawals_${netId}_${token}_${amount}`,
keyPath: 'eid',
indexes: [
...minimalIndexes,
{
name: 'nullifierHash',
unique: true,
}, // keys on which the index is created
],
},
);
});
});
const idb = new IndexedDB({
dbName: `tornado_core_${netId}`,
stores,
});
await idb.initDB();
return idb;
} }

View File

@@ -1,14 +1,14 @@
import { fetchData } from './providers'; import { fetchData } from './providers';
export interface IPResult { export interface IPResult {
ip: string; ip: string;
iso?: string; iso?: string;
tor?: boolean; tor?: boolean;
} }
export async function fetchIp(ipEcho: string) { export async function fetchIp(ipEcho: string) {
return (await fetchData(ipEcho, { return (await fetchData(ipEcho, {
method: 'GET', method: 'GET',
timeout: 30000, timeout: 30000,
})) as IPResult; })) as IPResult;
} }

View File

@@ -1,5 +1,10 @@
import { Worker as NodeWorker } from 'worker_threads'; import { Worker as NodeWorker } from 'worker_threads';
import { MerkleTree, PartialMerkleTree, Element, TreeEdge } from '@tornado/fixed-merkle-tree'; import {
MerkleTree,
PartialMerkleTree,
Element,
TreeEdge,
} from '@tornado/fixed-merkle-tree';
import type { Tornado } from '@tornado/contracts'; import type { Tornado } from '@tornado/contracts';
import { isNode, toFixedHex } from './utils'; import { isNode, toFixedHex } from './utils';
import { mimc } from './mimc'; import { mimc } from './mimc';
@@ -8,195 +13,247 @@ import type { DepositsEvents } from './events';
import type { NetIdType } from './networkConfig'; import type { NetIdType } from './networkConfig';
export interface MerkleTreeConstructor extends DepositType { export interface MerkleTreeConstructor extends DepositType {
Tornado: Tornado; Tornado: Tornado;
commitmentHex?: string; commitmentHex?: string;
merkleTreeHeight?: number; merkleTreeHeight?: number;
emptyElement?: string; emptyElement?: string;
merkleWorkerPath?: string; merkleWorkerPath?: string;
} }
export class MerkleTreeService { export class MerkleTreeService {
currency: string; currency: string;
amount: string; amount: string;
netId: NetIdType; netId: NetIdType;
Tornado: Tornado; Tornado: Tornado;
commitmentHex?: string; commitmentHex?: string;
instanceName: string; instanceName: string;
merkleTreeHeight: number; merkleTreeHeight: number;
emptyElement: string; emptyElement: string;
merkleWorkerPath?: string; merkleWorkerPath?: string;
constructor({ constructor({
netId, netId,
amount, amount,
currency, currency,
Tornado, Tornado,
commitmentHex, commitmentHex,
merkleTreeHeight = 20, merkleTreeHeight = 20,
emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292', emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292',
merkleWorkerPath, merkleWorkerPath,
}: MerkleTreeConstructor) { }: MerkleTreeConstructor) {
const instanceName = `${netId}_${currency}_${amount}`; const instanceName = `${netId}_${currency}_${amount}`;
this.currency = currency; this.currency = currency;
this.amount = amount; this.amount = amount;
this.netId = Number(netId); this.netId = Number(netId);
this.Tornado = Tornado; this.Tornado = Tornado;
this.instanceName = instanceName; this.instanceName = instanceName;
this.commitmentHex = commitmentHex; this.commitmentHex = commitmentHex;
this.merkleTreeHeight = merkleTreeHeight; this.merkleTreeHeight = merkleTreeHeight;
this.emptyElement = emptyElement; this.emptyElement = emptyElement;
this.merkleWorkerPath = merkleWorkerPath; this.merkleWorkerPath = merkleWorkerPath;
} }
async createTree(events: Element[]) { async createTree(events: Element[]) {
const { hash: hashFunction } = await mimc.getHash(); const { hash: hashFunction } = await mimc.getHash();
if (this.merkleWorkerPath) { if (this.merkleWorkerPath) {
console.log('Using merkleWorker\n'); console.log('Using merkleWorker\n');
try { try {
if (isNode) { if (isNode) {
const merkleWorkerPromise = new Promise((resolve, reject) => { const merkleWorkerPromise = new Promise(
const worker = new NodeWorker(this.merkleWorkerPath as string, { (resolve, reject) => {
workerData: { const worker = new NodeWorker(
merkleTreeHeight: this.merkleTreeHeight, this.merkleWorkerPath as string,
elements: events, {
zeroElement: this.emptyElement, workerData: {
}, merkleTreeHeight: this.merkleTreeHeight,
}); elements: events,
worker.on('message', resolve); zeroElement: this.emptyElement,
worker.on('error', reject); },
worker.on('exit', (code) => { },
if (code !== 0) { );
reject(new Error(`Worker stopped with exit code ${code}`)); worker.on('message', resolve);
} worker.on('error', reject);
}); worker.on('exit', (code) => {
}) as Promise<string>; if (code !== 0) {
reject(
new Error(
`Worker stopped with exit code ${code}`,
),
);
}
});
},
) as Promise<string>;
return MerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction); return MerkleTree.deserialize(
} else { JSON.parse(await merkleWorkerPromise),
const merkleWorkerPromise = new Promise((resolve, reject) => { hashFunction,
// eslint-disable-next-line @typescript-eslint/no-explicit-any );
const worker = new (Worker as any)(this.merkleWorkerPath); } 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 }) => { worker.onmessage = (e: { data: string }) => {
resolve(e.data); resolve(e.data);
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
worker.onerror = (e: any) => { worker.onerror = (e: any) => {
reject(e); reject(e);
}; };
worker.postMessage({ worker.postMessage({
merkleTreeHeight: this.merkleTreeHeight, merkleTreeHeight: this.merkleTreeHeight,
elements: events, elements: events,
zeroElement: this.emptyElement, zeroElement: this.emptyElement,
}); });
}) as Promise<string>; },
) as Promise<string>;
return MerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction); return MerkleTree.deserialize(
JSON.parse(await merkleWorkerPromise),
hashFunction,
);
}
} catch (err) {
console.log(
'merkleWorker failed, falling back to synchronous merkle tree',
);
console.log(err);
}
} }
} catch (err) {
console.log('merkleWorker failed, falling back to synchronous merkle tree'); return new MerkleTree(this.merkleTreeHeight, events, {
console.log(err); zeroElement: this.emptyElement,
} hashFunction,
});
} }
return new MerkleTree(this.merkleTreeHeight, events, { async createPartialTree({
zeroElement: this.emptyElement, edge,
hashFunction, elements,
}); }: {
} edge: TreeEdge;
elements: Element[];
}) {
const { hash: hashFunction } = await mimc.getHash();
async createPartialTree({ edge, elements }: { edge: TreeEdge; elements: Element[] }) { if (this.merkleWorkerPath) {
const { hash: hashFunction } = await mimc.getHash(); console.log('Using merkleWorker\n');
if (this.merkleWorkerPath) { try {
console.log('Using merkleWorker\n'); 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>;
try { return PartialMerkleTree.deserialize(
if (isNode) { JSON.parse(await merkleWorkerPromise),
const merkleWorkerPromise = new Promise((resolve, reject) => { hashFunction,
const worker = new NodeWorker(this.merkleWorkerPath as string, { );
workerData: { } else {
merkleTreeHeight: this.merkleTreeHeight, const merkleWorkerPromise = new Promise(
edge, (resolve, reject) => {
elements, // eslint-disable-next-line @typescript-eslint/no-explicit-any
zeroElement: this.emptyElement, const worker = new (Worker as any)(
}, this.merkleWorkerPath,
}); );
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); worker.onmessage = (e: { data: string }) => {
} else { resolve(e.data);
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 }) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve(e.data); worker.onerror = (e: any) => {
}; reject(e);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any worker.postMessage({
worker.onerror = (e: any) => { merkleTreeHeight: this.merkleTreeHeight,
reject(e); edge,
}; elements,
zeroElement: this.emptyElement,
});
},
) as Promise<string>;
worker.postMessage({ return PartialMerkleTree.deserialize(
merkleTreeHeight: this.merkleTreeHeight, JSON.parse(await merkleWorkerPromise),
edge, hashFunction,
elements, );
zeroElement: this.emptyElement, }
}); } catch (err) {
}) as Promise<string>; console.log(
'merkleWorker failed, falling back to synchronous merkle tree',
return PartialMerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction); );
console.log(err);
}
} }
} catch (err) {
console.log('merkleWorker failed, falling back to synchronous merkle tree'); return new PartialMerkleTree(this.merkleTreeHeight, edge, elements, {
console.log(err); zeroElement: this.emptyElement,
} hashFunction,
});
} }
return new PartialMerkleTree(this.merkleTreeHeight, edge, elements, { async verifyTree(events: DepositsEvents[]) {
zeroElement: this.emptyElement, console.log(
hashFunction, `\nCreating deposit tree for ${this.netId} ${this.amount} ${this.currency.toUpperCase()} would take a while\n`,
}); );
}
async verifyTree(events: DepositsEvents[]) { const timeStart = Date.now();
console.log(
`\nCreating deposit tree for ${this.netId} ${this.amount} ${this.currency.toUpperCase()} would take a while\n`,
);
const timeStart = Date.now(); const tree = await this.createTree(
events.map(({ commitment }) => commitment),
);
const tree = await this.createTree(events.map(({ commitment }) => commitment)); const isKnownRoot = await this.Tornado.isKnownRoot(
toFixedHex(BigInt(tree.root)),
);
const isKnownRoot = await this.Tornado.isKnownRoot(toFixedHex(BigInt(tree.root))); if (!isKnownRoot) {
const errMsg = `Deposit Event ${this.netId} ${this.amount} ${this.currency} is invalid`;
throw new Error(errMsg);
}
if (!isKnownRoot) { console.log(
const errMsg = `Deposit Event ${this.netId} ${this.amount} ${this.currency} is invalid`; `\nCreated ${this.netId} ${this.amount} ${this.currency.toUpperCase()} tree in ${Date.now() - timeStart}ms\n`,
throw new Error(errMsg); );
return tree;
} }
console.log(
`\nCreated ${this.netId} ${this.amount} ${this.currency.toUpperCase()} tree in ${Date.now() - timeStart}ms\n`,
);
return tree;
}
} }

View File

@@ -1,70 +1,95 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import workerThreads from 'worker_threads'; import workerThreads from 'worker_threads';
import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree'; import {
MerkleTree,
Element,
TreeEdge,
PartialMerkleTree,
} from '@tornado/fixed-merkle-tree';
import { mimc } from './mimc'; import { mimc } from './mimc';
import { isNode } from './utils'; import { isNode } from './utils';
interface WorkData { interface WorkData {
merkleTreeHeight: number; merkleTreeHeight: number;
edge?: TreeEdge; edge?: TreeEdge;
elements: Element[]; elements: Element[];
zeroElement: string; zeroElement: string;
} }
async function nodePostWork() { async function nodePostWork() {
const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = workerThreads.workerData as WorkData;
if (edge) {
const merkleTree = new PartialMerkleTree(merkleTreeHeight, edge, elements, {
zeroElement,
hashFunction,
});
(workerThreads.parentPort as workerThreads.MessagePort).postMessage(merkleTree.toString());
return;
}
const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
zeroElement,
hashFunction,
});
(workerThreads.parentPort as workerThreads.MessagePort).postMessage(merkleTree.toString());
}
if (isNode && workerThreads) {
nodePostWork();
} else if (!isNode && typeof addEventListener === 'function' && typeof postMessage === 'function') {
addEventListener('message', async (e: any) => {
let data;
if (e.data) {
data = e.data;
} else {
data = e;
}
const { hash: hashFunction } = await mimc.getHash(); const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = data as WorkData; const { merkleTreeHeight, edge, elements, zeroElement } =
workerThreads.workerData as WorkData;
if (edge) { if (edge) {
const merkleTree = new PartialMerkleTree(merkleTreeHeight, edge, elements, { const merkleTree = new PartialMerkleTree(
zeroElement, merkleTreeHeight,
hashFunction, edge,
}); elements,
{
zeroElement,
hashFunction,
},
);
postMessage(merkleTree.toString()); (workerThreads.parentPort as workerThreads.MessagePort).postMessage(
return; merkleTree.toString(),
);
return;
} }
const merkleTree = new MerkleTree(merkleTreeHeight, elements, { const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
zeroElement, zeroElement,
hashFunction, hashFunction,
}); });
postMessage(merkleTree.toString()); (workerThreads.parentPort as workerThreads.MessagePort).postMessage(
}); merkleTree.toString(),
} else { );
throw new Error('This browser / environment does not support workers!'); }
if (isNode && workerThreads) {
nodePostWork();
} else if (
!isNode &&
typeof addEventListener === 'function' &&
typeof postMessage === 'function'
) {
addEventListener('message', async (e: any) => {
let data;
if (e.data) {
data = e.data;
} else {
data = e;
}
const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } =
data as WorkData;
if (edge) {
const merkleTree = new PartialMerkleTree(
merkleTreeHeight,
edge,
elements,
{
zeroElement,
hashFunction,
},
);
postMessage(merkleTree.toString());
return;
}
const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
zeroElement,
hashFunction,
});
postMessage(merkleTree.toString());
});
} else {
throw new Error('This browser / environment does not support workers!');
} }

View File

@@ -2,27 +2,30 @@ import { MimcSponge, buildMimcSponge } from 'circomlibjs';
import type { Element, HashFunction } from '@tornado/fixed-merkle-tree'; import type { Element, HashFunction } from '@tornado/fixed-merkle-tree';
export class Mimc { export class Mimc {
sponge?: MimcSponge; sponge?: MimcSponge;
hash?: HashFunction<Element>; hash?: HashFunction<Element>;
mimcPromise: Promise<void>; mimcPromise: Promise<void>;
constructor() { constructor() {
this.mimcPromise = this.initMimc(); this.mimcPromise = this.initMimc();
} }
async initMimc() { async initMimc() {
this.sponge = await buildMimcSponge(); this.sponge = await buildMimcSponge();
this.hash = (left, right) => this.sponge?.F.toString(this.sponge?.multiHash([BigInt(left), BigInt(right)])); this.hash = (left, right) =>
} this.sponge?.F.toString(
this.sponge?.multiHash([BigInt(left), BigInt(right)]),
);
}
async getHash() { async getHash() {
await this.mimcPromise; await this.mimcPromise;
return { return {
sponge: this.sponge, sponge: this.sponge,
hash: this.hash, hash: this.hash,
}; };
} }
} }
export const mimc = new Mimc(); export const mimc = new Mimc();

View File

@@ -2,36 +2,44 @@ import { BaseContract, Interface } from 'ethers';
import { Multicall } from './typechain'; import { Multicall } from './typechain';
export interface Call3 { export interface Call3 {
contract?: BaseContract; contract?: BaseContract;
address?: string; address?: string;
interface?: Interface; interface?: Interface;
name: string; name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any[]; params?: any[];
allowFailure?: boolean; allowFailure?: boolean;
} }
export async function multicall(Multicall: Multicall, calls: Call3[]) { export async function multicall(Multicall: Multicall, calls: Call3[]) {
const calldata = calls.map((call) => { const calldata = calls.map((call) => {
const target = (call.contract?.target || call.address) as string; const target = (call.contract?.target || call.address) as string;
const callInterface = (call.contract?.interface || call.interface) as Interface; const callInterface = (call.contract?.interface ||
call.interface) as Interface;
return { return {
target, target,
callData: callInterface.encodeFunctionData(call.name, call.params), callData: callInterface.encodeFunctionData(call.name, call.params),
allowFailure: call.allowFailure ?? false, allowFailure: call.allowFailure ?? false,
}; };
}); });
const returnData = await Multicall.aggregate3.staticCall(calldata); const returnData = await Multicall.aggregate3.staticCall(calldata);
const res = returnData.map((call, i) => { const res = returnData.map((call, i) => {
const callInterface = (calls[i].contract?.interface || calls[i].interface) as Interface; const callInterface = (calls[i].contract?.interface ||
const [result, data] = call; calls[i].interface) as Interface;
const decodeResult = const [result, data] = call;
result && data && data !== '0x' ? callInterface.decodeFunctionResult(calls[i].name, data) : null; const decodeResult =
return !decodeResult ? null : decodeResult.length === 1 ? decodeResult[0] : decodeResult; result && data && data !== '0x'
}); ? callInterface.decodeFunctionResult(calls[i].name, data)
: null;
return !decodeResult
? null
: decodeResult.length === 1
? decodeResult[0]
: decodeResult;
});
return res; return res;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,34 @@
import { BabyJub, PedersenHash, Point, buildPedersenHash } from 'circomlibjs'; import { BabyJub, PedersenHash, Point, buildPedersenHash } from 'circomlibjs';
export class Pedersen { export class Pedersen {
pedersenHash?: PedersenHash; pedersenHash?: PedersenHash;
babyJub?: BabyJub; babyJub?: BabyJub;
pedersenPromise: Promise<void>; pedersenPromise: Promise<void>;
constructor() { constructor() {
this.pedersenPromise = this.initPedersen(); this.pedersenPromise = this.initPedersen();
} }
async initPedersen() { async initPedersen() {
this.pedersenHash = await buildPedersenHash(); this.pedersenHash = await buildPedersenHash();
this.babyJub = this.pedersenHash.babyJub; this.babyJub = this.pedersenHash.babyJub;
} }
async unpackPoint(buffer: Uint8Array) { async unpackPoint(buffer: Uint8Array) {
await this.pedersenPromise; await this.pedersenPromise;
return this.babyJub?.unpackPoint(this.pedersenHash?.hash(buffer) as Uint8Array); return this.babyJub?.unpackPoint(
} this.pedersenHash?.hash(buffer) as Uint8Array,
);
}
toStringBuffer(buffer: Uint8Array): string { toStringBuffer(buffer: Uint8Array): string {
return this.babyJub?.F.toString(buffer); return this.babyJub?.F.toString(buffer);
} }
} }
export const pedersen = new Pedersen(); export const pedersen = new Pedersen();
export async function buffPedersenHash(buffer: Uint8Array): Promise<string> { export async function buffPedersenHash(buffer: Uint8Array): Promise<string> {
const [hash] = (await pedersen.unpackPoint(buffer)) as Point; const [hash] = (await pedersen.unpackPoint(buffer)) as Point;
return pedersen.toStringBuffer(hash); return pedersen.toStringBuffer(hash);
} }

View File

@@ -1,28 +1,33 @@
import { ERC20Permit, ERC20Mock, TORN, PermitTornado } from '@tornado/contracts';
import { import {
BaseContract, ERC20Permit,
MaxUint256, ERC20Mock,
Provider, TORN,
Signature, PermitTornado,
Signer, } from '@tornado/contracts';
solidityPackedKeccak256, import {
TypedDataEncoder, BaseContract,
TypedDataField, MaxUint256,
Provider,
Signature,
Signer,
solidityPackedKeccak256,
TypedDataEncoder,
TypedDataField,
} from 'ethers'; } from 'ethers';
import { rBigInt } from './utils'; import { rBigInt } from './utils';
export interface PermitValue { export interface PermitValue {
spender: string; spender: string;
value: bigint; value: bigint;
nonce?: bigint; nonce?: bigint;
deadline?: bigint; deadline?: bigint;
} }
export interface PermitCommitments { export interface PermitCommitments {
denomination: bigint; denomination: bigint;
commitments: string[]; commitments: string[];
nonce?: bigint; nonce?: bigint;
deadline?: bigint; deadline?: bigint;
} }
export const permit2Address = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; export const permit2Address = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
@@ -31,212 +36,220 @@ export const permit2Address = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
* From @uniswap/permit2-sdk ported for ethers.js v6 * From @uniswap/permit2-sdk ported for ethers.js v6
*/ */
export interface Witness { export interface Witness {
witnessTypeName: string; witnessTypeName: string;
witnessType: { witnessType: {
[key: string]: TypedDataField[]; [key: string]: TypedDataField[];
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
witness: any; witness: any;
} }
export async function getPermitSignature({ export async function getPermitSignature({
Token,
signer,
spender,
value,
nonce,
deadline,
}: PermitValue & {
Token: ERC20Permit | ERC20Mock | TORN;
signer?: Signer;
}) {
const sigSigner = (signer || Token.runner) as Signer & { address: string };
const provider = sigSigner.provider as Provider;
const [name, lastNonce, { chainId }] = await Promise.all([
Token.name(),
Token.nonces(sigSigner.address),
provider.getNetwork(),
]);
const DOMAIN_SEPARATOR = {
name,
version: '1',
chainId,
verifyingContract: Token.target as string,
};
const PERMIT_TYPE = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
return Signature.from(
await sigSigner.signTypedData(DOMAIN_SEPARATOR, PERMIT_TYPE, {
owner: sigSigner.address,
spender,
value,
nonce: nonce || lastNonce,
deadline: deadline || MaxUint256,
}),
);
}
export async function getPermitCommitmentsSignature({
PermitTornado,
Token,
signer,
denomination,
commitments,
nonce,
}: PermitCommitments & {
PermitTornado: PermitTornado;
Token: ERC20Permit | ERC20Mock | TORN;
signer?: Signer;
}) {
const value = BigInt(commitments.length) * denomination;
const commitmentsHash = solidityPackedKeccak256(['bytes32[]'], [commitments]);
return await getPermitSignature({
Token, Token,
signer, signer,
spender: PermitTornado.target as string,
value,
nonce,
deadline: BigInt(commitmentsHash),
});
}
export async function getPermit2Signature({
Token,
signer,
spender,
value: amount,
nonce,
deadline,
witness,
}: PermitValue & {
Token: BaseContract;
signer?: Signer;
witness?: Witness;
}) {
const sigSigner = (signer || Token.runner) as Signer & { address: string };
const provider = sigSigner.provider as Provider;
const domain = {
name: 'Permit2',
chainId: (await provider.getNetwork()).chainId,
verifyingContract: permit2Address,
};
const types: {
[key: string]: TypedDataField[];
} = !witness
? {
PermitTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
}
: {
PermitWitnessTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
{ name: 'witness', type: witness.witnessTypeName },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
...witness.witnessType,
};
const values: {
permitted: {
token: string;
amount: bigint;
};
spender: string;
nonce: bigint;
deadline: bigint;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
witness?: any;
} = {
permitted: {
token: Token.target as string,
amount,
},
spender, spender,
// Sorted nonce are not required for Permit2
nonce: nonce || rBigInt(16),
deadline: deadline || MaxUint256,
};
if (witness) {
values.witness = witness.witness;
}
const hash = new TypedDataEncoder(types).hash(values);
const signature = Signature.from(await sigSigner.signTypedData(domain, types, values));
return {
domain,
types,
values,
hash,
signature,
};
}
export async function getPermit2CommitmentsSignature({
PermitTornado,
Token,
signer,
denomination,
commitments,
nonce,
deadline,
}: PermitCommitments & {
PermitTornado: PermitTornado;
Token: BaseContract;
signer?: Signer;
}) {
const value = BigInt(commitments.length) * denomination;
const commitmentsHash = solidityPackedKeccak256(['bytes32[]'], [commitments]);
return await getPermit2Signature({
Token,
signer,
spender: PermitTornado.target as string,
value, value,
nonce, nonce,
deadline, deadline,
witness: { }: PermitValue & {
witnessTypeName: 'PermitCommitments', Token: ERC20Permit | ERC20Mock | TORN;
witnessType: { signer?: Signer;
PermitCommitments: [ }) {
{ name: 'instance', type: 'address' }, const sigSigner = (signer || Token.runner) as Signer & { address: string };
{ name: 'commitmentsHash', type: 'bytes32' }, const provider = sigSigner.provider as Provider;
const [name, lastNonce, { chainId }] = await Promise.all([
Token.name(),
Token.nonces(sigSigner.address),
provider.getNetwork(),
]);
const DOMAIN_SEPARATOR = {
name,
version: '1',
chainId,
verifyingContract: Token.target as string,
};
const PERMIT_TYPE = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
], ],
}, };
witness: {
instance: PermitTornado.target, return Signature.from(
commitmentsHash, await sigSigner.signTypedData(DOMAIN_SEPARATOR, PERMIT_TYPE, {
}, owner: sigSigner.address,
}, spender,
}); value,
nonce: nonce || lastNonce,
deadline: deadline || MaxUint256,
}),
);
}
export async function getPermitCommitmentsSignature({
PermitTornado,
Token,
signer,
denomination,
commitments,
nonce,
}: PermitCommitments & {
PermitTornado: PermitTornado;
Token: ERC20Permit | ERC20Mock | TORN;
signer?: Signer;
}) {
const value = BigInt(commitments.length) * denomination;
const commitmentsHash = solidityPackedKeccak256(
['bytes32[]'],
[commitments],
);
return await getPermitSignature({
Token,
signer,
spender: PermitTornado.target as string,
value,
nonce,
deadline: BigInt(commitmentsHash),
});
}
export async function getPermit2Signature({
Token,
signer,
spender,
value: amount,
nonce,
deadline,
witness,
}: PermitValue & {
Token: BaseContract;
signer?: Signer;
witness?: Witness;
}) {
const sigSigner = (signer || Token.runner) as Signer & { address: string };
const provider = sigSigner.provider as Provider;
const domain = {
name: 'Permit2',
chainId: (await provider.getNetwork()).chainId,
verifyingContract: permit2Address,
};
const types: {
[key: string]: TypedDataField[];
} = !witness
? {
PermitTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
}
: {
PermitWitnessTransferFrom: [
{ name: 'permitted', type: 'TokenPermissions' },
{ name: 'spender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
{ name: 'witness', type: witness.witnessTypeName },
],
TokenPermissions: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
...witness.witnessType,
};
const values: {
permitted: {
token: string;
amount: bigint;
};
spender: string;
nonce: bigint;
deadline: bigint;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
witness?: any;
} = {
permitted: {
token: Token.target as string,
amount,
},
spender,
// Sorted nonce are not required for Permit2
nonce: nonce || rBigInt(16),
deadline: deadline || MaxUint256,
};
if (witness) {
values.witness = witness.witness;
}
const hash = new TypedDataEncoder(types).hash(values);
const signature = Signature.from(
await sigSigner.signTypedData(domain, types, values),
);
return {
domain,
types,
values,
hash,
signature,
};
}
export async function getPermit2CommitmentsSignature({
PermitTornado,
Token,
signer,
denomination,
commitments,
nonce,
deadline,
}: PermitCommitments & {
PermitTornado: PermitTornado;
Token: BaseContract;
signer?: Signer;
}) {
const value = BigInt(commitments.length) * denomination;
const commitmentsHash = solidityPackedKeccak256(
['bytes32[]'],
[commitments],
);
return await getPermit2Signature({
Token,
signer,
spender: PermitTornado.target as string,
value,
nonce,
deadline,
witness: {
witnessTypeName: 'PermitCommitments',
witnessType: {
PermitCommitments: [
{ name: 'instance', type: 'address' },
{ name: 'commitmentsHash', type: 'bytes32' },
],
},
witness: {
instance: PermitTornado.target,
commitmentsHash,
},
},
});
} }

View File

@@ -3,99 +3,123 @@ import { ERC20__factory, OffchainOracle, Multicall } from './typechain';
import { multicall, Call3 } from './multicall'; import { multicall, Call3 } from './multicall';
export class TokenPriceOracle { export class TokenPriceOracle {
oracle?: OffchainOracle; oracle?: OffchainOracle;
multicall: Multicall; multicall: Multicall;
provider: Provider; provider: Provider;
fallbackPrice: bigint; fallbackPrice: bigint;
constructor(provider: Provider, multicall: Multicall, oracle?: OffchainOracle) { constructor(
this.provider = provider; provider: Provider,
this.multicall = multicall; multicall: Multicall,
this.oracle = oracle; oracle?: OffchainOracle,
this.fallbackPrice = parseEther('0.0001'); ) {
} this.provider = provider;
this.multicall = multicall;
buildCalls( this.oracle = oracle;
tokens: { this.fallbackPrice = parseEther('0.0001');
tokenAddress: string;
decimals: number;
}[],
): Call3[] {
return tokens.map(({ tokenAddress }) => ({
contract: this.oracle,
name: 'getRateToEth',
params: [tokenAddress, true],
allowFailure: true,
}));
}
buildStable(stablecoinAddress: string): Call3[] {
const stablecoin = ERC20__factory.connect(stablecoinAddress, this.provider);
return [
{
contract: stablecoin,
name: 'decimals',
},
{
contract: this.oracle,
name: 'getRateToEth',
params: [stablecoin.target, true],
allowFailure: true,
},
];
}
async fetchPrice(tokenAddress: string, decimals: number): Promise<bigint> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) => resolve(this.fallbackPrice));
} }
try { buildCalls(
const price = await this.oracle.getRateToEth(tokenAddress, true); tokens: {
tokenAddress: string;
return (price * BigInt(10 ** decimals)) / BigInt(10 ** 18); decimals: number;
} catch (err) { }[],
console.log(`Failed to fetch oracle price for ${tokenAddress}, will use fallback price ${this.fallbackPrice}`); ): Call3[] {
console.log(err); return tokens.map(({ tokenAddress }) => ({
return this.fallbackPrice; contract: this.oracle,
} name: 'getRateToEth',
} params: [tokenAddress, true],
allowFailure: true,
async fetchPrices( }));
tokens: {
tokenAddress: string;
decimals: number;
}[],
): Promise<bigint[]> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) => resolve(tokens.map(() => this.fallbackPrice)));
} }
const prices = (await multicall(this.multicall, this.buildCalls(tokens))) as (bigint | null)[]; buildStable(stablecoinAddress: string): Call3[] {
const stablecoin = ERC20__factory.connect(
stablecoinAddress,
this.provider,
);
return prices.map((price, index) => { return [
if (!price) { {
price = this.fallbackPrice; contract: stablecoin,
} name: 'decimals',
return (price * BigInt(10 ** tokens[index].decimals)) / BigInt(10 ** 18); },
}); {
} contract: this.oracle,
name: 'getRateToEth',
async fetchEthUSD(stablecoinAddress: string): Promise<number> { params: [stablecoin.target, true],
// setup mock price for testnets allowFailure: true,
if (!this.oracle) { },
return new Promise((resolve) => resolve(10 ** 18 / Number(this.fallbackPrice))); ];
} }
const [decimals, price] = await multicall(this.multicall, this.buildStable(stablecoinAddress)); async fetchPrice(tokenAddress: string, decimals: number): Promise<bigint> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) => resolve(this.fallbackPrice));
}
// eth wei price of usdc token try {
const ethPrice = ((price || this.fallbackPrice) * BigInt(10n ** decimals)) / BigInt(10 ** 18); const price = await this.oracle.getRateToEth(tokenAddress, true);
return 1 / Number(formatEther(ethPrice)); return (price * BigInt(10 ** decimals)) / BigInt(10 ** 18);
} } catch (err) {
console.log(
`Failed to fetch oracle price for ${tokenAddress}, will use fallback price ${this.fallbackPrice}`,
);
console.log(err);
return this.fallbackPrice;
}
}
async fetchPrices(
tokens: {
tokenAddress: string;
decimals: number;
}[],
): Promise<bigint[]> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) =>
resolve(tokens.map(() => this.fallbackPrice)),
);
}
const prices = (await multicall(
this.multicall,
this.buildCalls(tokens),
)) as (bigint | null)[];
return prices.map((price, index) => {
if (!price) {
price = this.fallbackPrice;
}
return (
(price * BigInt(10 ** tokens[index].decimals)) /
BigInt(10 ** 18)
);
});
}
async fetchEthUSD(stablecoinAddress: string): Promise<number> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) =>
resolve(10 ** 18 / Number(this.fallbackPrice)),
);
}
const [decimals, price] = await multicall(
this.multicall,
this.buildStable(stablecoinAddress),
);
// eth wei price of usdc token
const ethPrice =
((price || this.fallbackPrice) * BigInt(10n ** decimals)) /
BigInt(10 ** 18);
return 1 / Number(formatEther(ethPrice));
}
} }

View File

@@ -2,470 +2,531 @@ import type { EventEmitter } from 'stream';
import type { RequestOptions } from 'http'; import type { RequestOptions } from 'http';
import crossFetch from 'cross-fetch'; import crossFetch from 'cross-fetch';
import { import {
FetchRequest, FetchRequest,
JsonRpcApiProvider, JsonRpcApiProvider,
JsonRpcProvider, JsonRpcProvider,
Wallet, Wallet,
HDNodeWallet, HDNodeWallet,
FetchGetUrlFunc, FetchGetUrlFunc,
Provider, Provider,
SigningKey, SigningKey,
TransactionRequest, TransactionRequest,
JsonRpcSigner, JsonRpcSigner,
BrowserProvider, BrowserProvider,
Networkish, Networkish,
Eip1193Provider, Eip1193Provider,
VoidSigner, VoidSigner,
Network, Network,
EnsPlugin, EnsPlugin,
GasCostPlugin, GasCostPlugin,
} from 'ethers'; } from 'ethers';
import type { RequestInfo, RequestInit, Response, HeadersInit } from 'node-fetch'; import type {
RequestInfo,
RequestInit,
Response,
HeadersInit,
} from 'node-fetch';
// Temporary workaround until @types/node-fetch is compatible with @types/node // Temporary workaround until @types/node-fetch is compatible with @types/node
import type { AbortSignal as FetchAbortSignal } from 'node-fetch/externals'; import type { AbortSignal as FetchAbortSignal } from 'node-fetch/externals';
import { isNode, sleep } from './utils'; import { isNode, sleep } from './utils';
import type { Config, NetIdType } from './networkConfig'; import type { Config, NetIdType } from './networkConfig';
declare global { declare global {
interface Window { interface Window {
ethereum?: Eip1193Provider & EventEmitter; ethereum?: Eip1193Provider & EventEmitter;
} }
} }
// Update this for every Tor Browser release // Update this for every Tor Browser release
export const defaultUserAgent = 'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0'; export const defaultUserAgent =
'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0';
export const fetch = crossFetch as unknown as nodeFetch; export const fetch = crossFetch as unknown as nodeFetch;
export type nodeFetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>; export type nodeFetch = (
url: RequestInfo,
init?: RequestInit,
) => Promise<Response>;
export type fetchDataOptions = RequestInit & { export type fetchDataOptions = RequestInit & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
headers?: HeadersInit | any; headers?: HeadersInit | any;
maxRetry?: number; maxRetry?: number;
retryOn?: number; retryOn?: number;
userAgent?: string; userAgent?: string;
timeout?: number; timeout?: number;
proxy?: string; proxy?: string;
torPort?: number; torPort?: number;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
debug?: Function; debug?: Function;
returnResponse?: boolean; returnResponse?: boolean;
}; };
export type NodeAgent = RequestOptions['agent'] | ((parsedUrl: URL) => RequestOptions['agent']); export type NodeAgent =
| RequestOptions['agent']
| ((parsedUrl: URL) => RequestOptions['agent']);
export function getHttpAgent({ export function getHttpAgent({
fetchUrl, fetchUrl,
proxyUrl, proxyUrl,
torPort, torPort,
retry, retry,
}: { }: {
fetchUrl: string; fetchUrl: string;
proxyUrl?: string; proxyUrl?: string;
torPort?: number; torPort?: number;
retry: number; retry: number;
}): NodeAgent | undefined { }): NodeAgent | undefined {
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const { HttpProxyAgent } = require('http-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent');
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpsProxyAgent } = require('https-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent');
/* eslint-enable @typescript-eslint/no-require-imports */ /* eslint-enable @typescript-eslint/no-require-imports */
if (torPort) { if (torPort) {
return new SocksProxyAgent(`socks5h://tor${retry}@127.0.0.1:${torPort}`); return new SocksProxyAgent(
} `socks5h://tor${retry}@127.0.0.1:${torPort}`,
);
if (!proxyUrl) { }
return;
} if (!proxyUrl) {
return;
const isHttps = fetchUrl.includes('https://'); }
if (proxyUrl.includes('socks://') || proxyUrl.includes('socks4://') || proxyUrl.includes('socks5://')) { const isHttps = fetchUrl.includes('https://');
return new SocksProxyAgent(proxyUrl);
} if (
proxyUrl.includes('socks://') ||
if (proxyUrl.includes('http://') || proxyUrl.includes('https://')) { proxyUrl.includes('socks4://') ||
if (isHttps) { proxyUrl.includes('socks5://')
return new HttpsProxyAgent(proxyUrl); ) {
return new SocksProxyAgent(proxyUrl);
}
if (proxyUrl.includes('http://') || proxyUrl.includes('https://')) {
if (isHttps) {
return new HttpsProxyAgent(proxyUrl);
}
return new HttpProxyAgent(proxyUrl);
} }
return new HttpProxyAgent(proxyUrl);
}
} }
export async function fetchData(url: string, options: fetchDataOptions = {}) { export async function fetchData(url: string, options: fetchDataOptions = {}) {
const MAX_RETRY = options.maxRetry ?? 3; const MAX_RETRY = options.maxRetry ?? 3;
const RETRY_ON = options.retryOn ?? 500; const RETRY_ON = options.retryOn ?? 500;
const userAgent = options.userAgent ?? defaultUserAgent; const userAgent = options.userAgent ?? defaultUserAgent;
let retry = 0; let retry = 0;
let errorObject; let errorObject;
if (!options.method) { if (!options.method) {
if (!options.body) { if (!options.body) {
options.method = 'GET'; options.method = 'GET';
} else { } else {
options.method = 'POST'; options.method = 'POST';
} }
}
if (!options.headers) {
options.headers = {};
}
if (isNode && !options.headers['User-Agent']) {
options.headers['User-Agent'] = userAgent;
}
while (retry < MAX_RETRY + 1) {
let timeout;
// Define promise timeout when the options.timeout is available
if (!options.signal && options.timeout) {
const controller = new AbortController();
// Temporary workaround until @types/node-fetch is compatible with @types/node
options.signal = controller.signal as FetchAbortSignal;
// Define timeout in seconds
timeout = setTimeout(() => {
controller.abort();
}, options.timeout);
} }
if (!options.agent && isNode && (options.proxy || options.torPort)) { if (!options.headers) {
options.agent = getHttpAgent({ options.headers = {};
fetchUrl: url, }
proxyUrl: options.proxy,
torPort: options.torPort, if (isNode && !options.headers['User-Agent']) {
retry, options.headers['User-Agent'] = userAgent;
}); }
while (retry < MAX_RETRY + 1) {
let timeout;
// Define promise timeout when the options.timeout is available
if (!options.signal && options.timeout) {
const controller = new AbortController();
// Temporary workaround until @types/node-fetch is compatible with @types/node
options.signal = controller.signal as FetchAbortSignal;
// Define timeout in seconds
timeout = setTimeout(() => {
controller.abort();
}, options.timeout);
}
if (!options.agent && isNode && (options.proxy || options.torPort)) {
options.agent = getHttpAgent({
fetchUrl: url,
proxyUrl: options.proxy,
torPort: options.torPort,
retry,
});
}
if (options.debug && typeof options.debug === 'function') {
options.debug('request', {
url,
retry,
errorObject,
options,
});
}
try {
const resp = await fetch(url, {
method: options.method,
headers: options.headers,
body: options.body,
redirect: options.redirect,
signal: options.signal,
agent: options.agent,
});
if (options.debug && typeof options.debug === 'function') {
options.debug('response', resp);
}
if (!resp.ok) {
const errMsg =
`Request to ${url} failed with error code ${resp.status}:\n` +
(await resp.text());
throw new Error(errMsg);
}
if (options.returnResponse) {
return resp;
}
const contentType = resp.headers.get('content-type');
// If server returns JSON object, parse it and return as an object
if (contentType?.includes('application/json')) {
return await resp.json();
}
// Else if the server returns text parse it as a string
if (contentType?.includes('text')) {
return await resp.text();
}
// Return as a response object https://developer.mozilla.org/en-US/docs/Web/API/Response
return resp;
} catch (error) {
if (timeout) {
clearTimeout(timeout);
}
errorObject = error;
retry++;
await sleep(RETRY_ON);
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
} }
if (options.debug && typeof options.debug === 'function') { if (options.debug && typeof options.debug === 'function') {
options.debug('request', { options.debug('error', errorObject);
url,
retry,
errorObject,
options,
});
} }
try { throw errorObject;
const resp = await fetch(url, {
method: options.method,
headers: options.headers,
body: options.body,
redirect: options.redirect,
signal: options.signal,
agent: options.agent,
});
if (options.debug && typeof options.debug === 'function') {
options.debug('response', resp);
}
if (!resp.ok) {
const errMsg = `Request to ${url} failed with error code ${resp.status}:\n` + (await resp.text());
throw new Error(errMsg);
}
if (options.returnResponse) {
return resp;
}
const contentType = resp.headers.get('content-type');
// If server returns JSON object, parse it and return as an object
if (contentType?.includes('application/json')) {
return await resp.json();
}
// Else if the server returns text parse it as a string
if (contentType?.includes('text')) {
return await resp.text();
}
// Return as a response object https://developer.mozilla.org/en-US/docs/Web/API/Response
return resp;
} catch (error) {
if (timeout) {
clearTimeout(timeout);
}
errorObject = error;
retry++;
await sleep(RETRY_ON);
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
if (options.debug && typeof options.debug === 'function') {
options.debug('error', errorObject);
}
throw errorObject;
} }
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export const fetchGetUrlFunc = export const fetchGetUrlFunc =
(options: fetchDataOptions = {}): FetchGetUrlFunc => (options: fetchDataOptions = {}): FetchGetUrlFunc =>
async (req, _signal) => { async (req, _signal) => {
let signal; let signal;
if (_signal) { if (_signal) {
const controller = new AbortController(); const controller = new AbortController();
// Temporary workaround until @types/node-fetch is compatible with @types/node // Temporary workaround until @types/node-fetch is compatible with @types/node
signal = controller.signal as FetchAbortSignal; signal = controller.signal as FetchAbortSignal;
_signal.addListener(() => { _signal.addListener(() => {
controller.abort(); controller.abort();
}); });
} }
const init = { const init = {
...options, ...options,
method: req.method || 'POST', method: req.method || 'POST',
headers: req.headers, headers: req.headers,
body: req.body || undefined, body: req.body || undefined,
signal, signal,
returnResponse: true, 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;
});
const respBody = await resp.arrayBuffer();
const body = respBody == null ? null : new Uint8Array(respBody);
return {
statusCode: resp.status,
statusMessage: resp.statusText,
headers,
body,
};
}; };
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 */ /* eslint-enable @typescript-eslint/no-explicit-any */
export type getProviderOptions = fetchDataOptions & { export type getProviderOptions = fetchDataOptions & {
// NetId to check against rpc // NetId to check against rpc
netId?: NetIdType; netId?: NetIdType;
pollingInterval?: number; pollingInterval?: number;
}; };
export async function getProvider(rpcUrl: string, fetchOptions?: getProviderOptions): Promise<JsonRpcProvider> { export async function getProvider(
const fetchReq = new FetchRequest(rpcUrl); rpcUrl: string,
fetchOptions?: getProviderOptions,
): Promise<JsonRpcProvider> {
const fetchReq = new FetchRequest(rpcUrl);
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions); fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
const staticNetwork = await new JsonRpcProvider(fetchReq).getNetwork(); const staticNetwork = await new JsonRpcProvider(fetchReq).getNetwork();
const chainId = Number(staticNetwork.chainId); const chainId = Number(staticNetwork.chainId);
if (fetchOptions?.netId && fetchOptions.netId !== chainId) { if (fetchOptions?.netId && fetchOptions.netId !== chainId) {
const errMsg = `Wrong network for ${rpcUrl}, wants ${fetchOptions.netId} got ${chainId}`; const errMsg = `Wrong network for ${rpcUrl}, wants ${fetchOptions.netId} got ${chainId}`;
throw new Error(errMsg); throw new Error(errMsg);
} }
return new JsonRpcProvider(fetchReq, staticNetwork, { return new JsonRpcProvider(fetchReq, staticNetwork, {
staticNetwork, staticNetwork,
pollingInterval: fetchOptions?.pollingInterval || 1000, pollingInterval: fetchOptions?.pollingInterval || 1000,
}); });
} }
export function getProviderWithNetId( export function getProviderWithNetId(
netId: NetIdType, netId: NetIdType,
rpcUrl: string, rpcUrl: string,
config: Config, config: Config,
fetchOptions?: getProviderOptions, fetchOptions?: getProviderOptions,
): JsonRpcProvider { ): JsonRpcProvider {
const { networkName, reverseRecordsContract, pollInterval } = config; const { networkName, reverseRecordsContract, pollInterval } = config;
const hasEns = Boolean(reverseRecordsContract); const hasEns = Boolean(reverseRecordsContract);
const fetchReq = new FetchRequest(rpcUrl); const fetchReq = new FetchRequest(rpcUrl);
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions); fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
const staticNetwork = new Network(networkName, netId); const staticNetwork = new Network(networkName, netId);
if (hasEns) { if (hasEns) {
staticNetwork.attachPlugin(new EnsPlugin(null, Number(netId))); staticNetwork.attachPlugin(new EnsPlugin(null, Number(netId)));
} }
staticNetwork.attachPlugin(new GasCostPlugin()); staticNetwork.attachPlugin(new GasCostPlugin());
const provider = new JsonRpcProvider(fetchReq, staticNetwork, { const provider = new JsonRpcProvider(fetchReq, staticNetwork, {
staticNetwork, staticNetwork,
pollingInterval: fetchOptions?.pollingInterval || pollInterval * 1000, pollingInterval: fetchOptions?.pollingInterval || pollInterval * 1000,
}); });
return provider; return provider;
} }
export const populateTransaction = async ( export const populateTransaction = async (
signer: TornadoWallet | TornadoVoidSigner | TornadoRpcSigner, signer: TornadoWallet | TornadoVoidSigner | TornadoRpcSigner,
tx: TransactionRequest, tx: TransactionRequest,
) => { ) => {
const provider = signer.provider as Provider; const provider = signer.provider as Provider;
if (!tx.from) { if (!tx.from) {
tx.from = signer.address; tx.from = signer.address;
} else if (tx.from !== signer.address) { } else if (tx.from !== signer.address) {
const errMsg = `populateTransaction: signer mismatch for tx, wants ${tx.from} have ${signer.address}`; const errMsg = `populateTransaction: signer mismatch for tx, wants ${tx.from} have ${signer.address}`;
throw new Error(errMsg); throw new Error(errMsg);
}
const [feeData, nonce] = await Promise.all([
tx.maxFeePerGas || tx.gasPrice ? undefined : provider.getFeeData(),
tx.nonce ? undefined : provider.getTransactionCount(signer.address, 'pending'),
]);
if (feeData) {
// EIP-1559
if (feeData.maxFeePerGas) {
if (!tx.type) {
tx.type = 2;
}
tx.maxFeePerGas = (feeData.maxFeePerGas * (BigInt(10000) + BigInt(signer.gasPriceBump))) / BigInt(10000);
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
delete tx.gasPrice;
} else if (feeData.gasPrice) {
if (!tx.type) {
tx.type = 0;
}
tx.gasPrice = feeData.gasPrice;
delete tx.maxFeePerGas;
delete tx.maxPriorityFeePerGas;
} }
}
if (nonce) { const [feeData, nonce] = await Promise.all([
tx.nonce = nonce; tx.maxFeePerGas || tx.gasPrice ? undefined : provider.getFeeData(),
} tx.nonce
? undefined
: provider.getTransactionCount(signer.address, 'pending'),
]);
if (!tx.gasLimit) { if (feeData) {
try { // EIP-1559
const gasLimit = await provider.estimateGas(tx); if (feeData.maxFeePerGas) {
if (!tx.type) {
tx.type = 2;
}
tx.gasLimit = tx.maxFeePerGas =
gasLimit === BigInt(21000) (feeData.maxFeePerGas *
? gasLimit (BigInt(10000) + BigInt(signer.gasPriceBump))) /
: (gasLimit * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000); BigInt(10000);
} catch (error) { tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
if (signer.gasFailover) { delete tx.gasPrice;
console.log('populateTransaction: warning gas estimation failed falling back to 3M gas'); } else if (feeData.gasPrice) {
// Gas failover if (!tx.type) {
tx.gasLimit = BigInt('3000000'); tx.type = 0;
} else { }
throw error; tx.gasPrice = feeData.gasPrice;
} delete tx.maxFeePerGas;
delete tx.maxPriorityFeePerGas;
}
} }
}
return tx; if (nonce) {
tx.nonce = nonce;
}
if (!tx.gasLimit) {
try {
const gasLimit = await provider.estimateGas(tx);
tx.gasLimit =
gasLimit === BigInt(21000)
? gasLimit
: (gasLimit *
(BigInt(10000) + BigInt(signer.gasLimitBump))) /
BigInt(10000);
} catch (error) {
if (signer.gasFailover) {
console.log(
'populateTransaction: warning gas estimation failed falling back to 3M gas',
);
// Gas failover
tx.gasLimit = BigInt('3000000');
} else {
throw error;
}
}
}
return tx;
}; };
export interface TornadoWalletOptions { export interface TornadoWalletOptions {
gasPriceBump?: number; gasPriceBump?: number;
gasLimitBump?: number; gasLimitBump?: number;
gasFailover?: boolean; gasFailover?: boolean;
bumpNonce?: boolean; bumpNonce?: boolean;
} }
export class TornadoWallet extends Wallet { export class TornadoWallet extends Wallet {
nonce?: number; nonce?: number;
gasPriceBump: number; gasPriceBump: number;
gasLimitBump: number; gasLimitBump: number;
gasFailover: boolean; gasFailover: boolean;
bumpNonce: boolean; bumpNonce: boolean;
constructor( constructor(
key: string | SigningKey, key: string | SigningKey,
provider?: Provider, provider?: Provider,
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {}, {
) { gasPriceBump,
super(key, provider); gasLimitBump,
// 10% bump from the recommended fee gasFailover,
this.gasPriceBump = gasPriceBump ?? 0; bumpNonce,
// 30% bump from the recommended gaslimit }: TornadoWalletOptions = {},
this.gasLimitBump = gasLimitBump ?? 3000; ) {
this.gasFailover = gasFailover ?? false; super(key, provider);
// Disable bump nonce feature unless being used by the server environment // 10% bump from the recommended fee
this.bumpNonce = bumpNonce ?? false; this.gasPriceBump = gasPriceBump ?? 0;
} // 30% bump from the recommended gaslimit
this.gasLimitBump = gasLimitBump ?? 3000;
this.gasFailover = gasFailover ?? false;
// Disable bump nonce feature unless being used by the server environment
this.bumpNonce = bumpNonce ?? false;
}
static fromMnemonic(mneomnic: string, provider: Provider, index = 0, options?: TornadoWalletOptions) { static fromMnemonic(
const defaultPath = `m/44'/60'/0'/0/${index}`; mneomnic: string,
const { privateKey } = HDNodeWallet.fromPhrase(mneomnic, undefined, defaultPath); provider: Provider,
return new TornadoWallet(privateKey as unknown as SigningKey, provider, options); index = 0,
} options?: TornadoWalletOptions,
) {
const defaultPath = `m/44'/60'/0'/0/${index}`;
const { privateKey } = HDNodeWallet.fromPhrase(
mneomnic,
undefined,
defaultPath,
);
return new TornadoWallet(
privateKey as unknown as SigningKey,
provider,
options,
);
}
async populateTransaction(tx: TransactionRequest) { async populateTransaction(tx: TransactionRequest) {
const txObject = await populateTransaction(this, tx); const txObject = await populateTransaction(this, tx);
this.nonce = Number(txObject.nonce); this.nonce = Number(txObject.nonce);
return super.populateTransaction(txObject); return super.populateTransaction(txObject);
} }
} }
export class TornadoVoidSigner extends VoidSigner { export class TornadoVoidSigner extends VoidSigner {
nonce?: number; nonce?: number;
gasPriceBump: number; gasPriceBump: number;
gasLimitBump: number; gasLimitBump: number;
gasFailover: boolean; gasFailover: boolean;
bumpNonce: boolean; bumpNonce: boolean;
constructor( constructor(
address: string, address: string,
provider?: Provider, provider?: Provider,
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {}, {
) { gasPriceBump,
super(address, provider); gasLimitBump,
// 10% bump from the recommended fee gasFailover,
this.gasPriceBump = gasPriceBump ?? 0; bumpNonce,
// 30% bump from the recommended gaslimit }: TornadoWalletOptions = {},
this.gasLimitBump = gasLimitBump ?? 3000; ) {
this.gasFailover = gasFailover ?? false; super(address, provider);
// turn off bumpNonce feature for view only wallet // 10% bump from the recommended fee
this.bumpNonce = bumpNonce ?? false; this.gasPriceBump = gasPriceBump ?? 0;
} // 30% bump from the recommended gaslimit
this.gasLimitBump = gasLimitBump ?? 3000;
this.gasFailover = gasFailover ?? false;
// turn off bumpNonce feature for view only wallet
this.bumpNonce = bumpNonce ?? false;
}
async populateTransaction(tx: TransactionRequest) { async populateTransaction(tx: TransactionRequest) {
const txObject = await populateTransaction(this, tx); const txObject = await populateTransaction(this, tx);
this.nonce = Number(txObject.nonce); this.nonce = Number(txObject.nonce);
return super.populateTransaction(txObject); return super.populateTransaction(txObject);
} }
} }
export class TornadoRpcSigner extends JsonRpcSigner { export class TornadoRpcSigner extends JsonRpcSigner {
nonce?: number; nonce?: number;
gasPriceBump: number; gasPriceBump: number;
gasLimitBump: number; gasLimitBump: number;
gasFailover: boolean; gasFailover: boolean;
bumpNonce: boolean; bumpNonce: boolean;
constructor( constructor(
provider: JsonRpcApiProvider, provider: JsonRpcApiProvider,
address: string, address: string,
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {}, {
) { gasPriceBump,
super(provider, address); gasLimitBump,
// 10% bump from the recommended fee gasFailover,
this.gasPriceBump = gasPriceBump ?? 0; bumpNonce,
// 30% bump from the recommended gaslimit }: TornadoWalletOptions = {},
this.gasLimitBump = gasLimitBump ?? 3000; ) {
this.gasFailover = gasFailover ?? false; super(provider, address);
// turn off bumpNonce feature for browser wallet // 10% bump from the recommended fee
this.bumpNonce = bumpNonce ?? false; this.gasPriceBump = gasPriceBump ?? 0;
} // 30% bump from the recommended gaslimit
this.gasLimitBump = gasLimitBump ?? 3000;
this.gasFailover = gasFailover ?? false;
// turn off bumpNonce feature for browser wallet
this.bumpNonce = bumpNonce ?? false;
}
async sendUncheckedTransaction(tx: TransactionRequest) { async sendUncheckedTransaction(tx: TransactionRequest) {
return super.sendUncheckedTransaction(await populateTransaction(this, tx)); return super.sendUncheckedTransaction(
} await populateTransaction(this, tx),
);
}
} }
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -475,43 +536,56 @@ export type handleWalletFunc = (...args: any[]) => void;
/* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-explicit-any */
export interface TornadoBrowserProviderOptions extends TornadoWalletOptions { export interface TornadoBrowserProviderOptions extends TornadoWalletOptions {
netId?: NetIdType; netId?: NetIdType;
connectWallet?: connectWalletFunc; connectWallet?: connectWalletFunc;
handleNetworkChanges?: handleWalletFunc; handleNetworkChanges?: handleWalletFunc;
handleAccountChanges?: handleWalletFunc; handleAccountChanges?: handleWalletFunc;
handleAccountDisconnect?: handleWalletFunc; handleAccountDisconnect?: handleWalletFunc;
} }
export class TornadoBrowserProvider extends BrowserProvider { export class TornadoBrowserProvider extends BrowserProvider {
options?: TornadoBrowserProviderOptions; options?: TornadoBrowserProviderOptions;
constructor(ethereum: Eip1193Provider, network?: Networkish, options?: TornadoBrowserProviderOptions) { constructor(
super(ethereum, network); ethereum: Eip1193Provider,
this.options = options; network?: Networkish,
} options?: TornadoBrowserProviderOptions,
async getSigner(address: string): Promise<TornadoRpcSigner> {
const signerAddress = (await super.getSigner(address)).address;
if (
this.options?.netId &&
this.options?.connectWallet &&
Number(await super.send('net_version', [])) !== this.options?.netId
) { ) {
await this.options.connectWallet(this.options?.netId); super(ethereum, network);
this.options = options;
} }
if (this.options?.handleNetworkChanges) { async getSigner(address: string): Promise<TornadoRpcSigner> {
window?.ethereum?.on('chainChanged', this.options.handleNetworkChanges); const signerAddress = (await super.getSigner(address)).address;
}
if (this.options?.handleAccountChanges) { if (
window?.ethereum?.on('accountsChanged', this.options.handleAccountChanges); this.options?.netId &&
} this.options?.connectWallet &&
Number(await super.send('net_version', [])) !== this.options?.netId
) {
await this.options.connectWallet(this.options?.netId);
}
if (this.options?.handleAccountDisconnect) { if (this.options?.handleNetworkChanges) {
window?.ethereum?.on('disconnect', this.options.handleAccountDisconnect); window?.ethereum?.on(
} 'chainChanged',
this.options.handleNetworkChanges,
);
}
return new TornadoRpcSigner(this, signerAddress, this.options); if (this.options?.handleAccountChanges) {
} window?.ethereum?.on(
'accountsChanged',
this.options.handleAccountChanges,
);
}
if (this.options?.handleAccountDisconnect) {
window?.ethereum?.on(
'disconnect',
this.options.handleAccountDisconnect,
);
}
return new TornadoRpcSigner(this, signerAddress, this.options);
}
} }

View File

@@ -13,88 +13,88 @@ export const MAX_FEE = 0.9;
export const MIN_STAKE_BALANCE = parseEther('500'); export const MIN_STAKE_BALANCE = parseEther('500');
export interface RelayerParams { export interface RelayerParams {
ensName: string; ensName: string;
relayerAddress: string; relayerAddress: string;
} }
/** /**
* Info from relayer status * Info from relayer status
*/ */
export interface RelayerInfo extends RelayerParams { export interface RelayerInfo extends RelayerParams {
netId: NetIdType; netId: NetIdType;
url: string; url: string;
hostname: string; hostname: string;
rewardAccount: string; rewardAccount: string;
instances: string[]; instances: string[];
stakeBalance?: string; stakeBalance?: string;
gasPrice?: number; gasPrice?: number;
ethPrices?: { ethPrices?: {
[key in string]: string; [key in string]: string;
}; };
currentQueue: number; currentQueue: number;
tornadoServiceFee: number; tornadoServiceFee: number;
} }
export interface RelayerError { export interface RelayerError {
hostname: string; hostname: string;
relayerAddress?: string; relayerAddress?: string;
errorMessage?: string; errorMessage?: string;
hasError: boolean; hasError: boolean;
} }
export interface RelayerStatus { export interface RelayerStatus {
url: string; url: string;
rewardAccount: string; rewardAccount: string;
instances: { instances: {
[key in string]: { [key in string]: {
instanceAddress: { instanceAddress: {
[key in string]: string; [key in string]: string;
}; };
tokenAddress?: string; tokenAddress?: string;
symbol: string; symbol: string;
decimals: number; decimals: number;
};
}; };
}; gasPrices?: {
gasPrices?: { fast: number;
fast: number; additionalProperties?: number;
additionalProperties?: number; };
}; netId: NetIdType;
netId: NetIdType; ethPrices?: {
ethPrices?: { [key in string]: string;
[key in string]: string; };
}; tornadoServiceFee: number;
tornadoServiceFee: number; latestBlock?: number;
latestBlock?: number; version: string;
version: string; health: {
health: { status: string;
status: string; error: string;
error: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any errorsLog: any[];
errorsLog: any[]; };
}; currentQueue: number;
currentQueue: number;
} }
export interface TornadoWithdrawParams extends snarkProofs { export interface TornadoWithdrawParams extends snarkProofs {
contract: string; contract: string;
} }
export interface RelayerTornadoWithdraw { export interface RelayerTornadoWithdraw {
id?: string; id?: string;
error?: string; error?: string;
} }
export interface RelayerTornadoJobs { export interface RelayerTornadoJobs {
error?: string; error?: string;
id: string; id: string;
type?: string; type?: string;
status: string; status: string;
contract?: string; contract?: string;
proof?: string; proof?: string;
args?: string[]; args?: string[];
txHash?: string; txHash?: string;
confirmations?: number; confirmations?: number;
failedReason?: string; failedReason?: string;
} }
/** /**
@@ -125,291 +125,327 @@ export function isRelayerUpdated(relayerVersion: string, netId: NetIdType) {
} }
**/ **/
export function calculateScore({ stakeBalance, tornadoServiceFee }: RelayerInfo) { export function calculateScore({
if (tornadoServiceFee < MIN_FEE) { stakeBalance,
tornadoServiceFee = MIN_FEE; tornadoServiceFee,
} else if (tornadoServiceFee >= MAX_FEE) { }: RelayerInfo) {
return BigInt(0); if (tornadoServiceFee < MIN_FEE) {
} tornadoServiceFee = MIN_FEE;
} else if (tornadoServiceFee >= MAX_FEE) {
return BigInt(0);
}
const serviceFeeCoefficient = (tornadoServiceFee - MIN_FEE) ** 2; const serviceFeeCoefficient = (tornadoServiceFee - MIN_FEE) ** 2;
const feeDiffCoefficient = 1 / (MAX_FEE - MIN_FEE) ** 2; const feeDiffCoefficient = 1 / (MAX_FEE - MIN_FEE) ** 2;
const coefficientsMultiplier = 1 - feeDiffCoefficient * serviceFeeCoefficient; const coefficientsMultiplier =
1 - feeDiffCoefficient * serviceFeeCoefficient;
return BigInt(Math.floor(Number(stakeBalance || '0') * coefficientsMultiplier)); return BigInt(
Math.floor(Number(stakeBalance || '0') * coefficientsMultiplier),
);
} }
export function getWeightRandom(weightsScores: bigint[], random: bigint) { export function getWeightRandom(weightsScores: bigint[], random: bigint) {
for (let i = 0; i < weightsScores.length; i++) { for (let i = 0; i < weightsScores.length; i++) {
if (random < weightsScores[i]) { if (random < weightsScores[i]) {
return i; return i;
}
random = random - weightsScores[i];
} }
random = random - weightsScores[i]; return Math.floor(Math.random() * weightsScores.length);
}
return Math.floor(Math.random() * weightsScores.length);
} }
export interface RelayerInstanceList { export interface RelayerInstanceList {
[key: string]: { [key: string]: {
instanceAddress: { instanceAddress: {
[key: string]: string; [key: string]: string;
};
}; };
};
} }
export function getSupportedInstances(instanceList: RelayerInstanceList) { export function getSupportedInstances(instanceList: RelayerInstanceList) {
const rawList = Object.values(instanceList) const rawList = Object.values(instanceList)
.map(({ instanceAddress }) => { .map(({ instanceAddress }) => {
return Object.values(instanceAddress); return Object.values(instanceAddress);
}) })
.flat(); .flat();
return rawList.map((l) => getAddress(l)); return rawList.map((l) => getAddress(l));
} }
export function pickWeightedRandomRelayer(relayers: RelayerInfo[]) { export function pickWeightedRandomRelayer(relayers: RelayerInfo[]) {
const weightsScores = relayers.map((el) => calculateScore(el)); const weightsScores = relayers.map((el) => calculateScore(el));
const totalWeight = weightsScores.reduce((acc, curr) => { const totalWeight = weightsScores.reduce((acc, curr) => {
return (acc = acc + curr); return (acc = acc + curr);
}, BigInt('0')); }, BigInt('0'));
const random = BigInt(Math.floor(Number(totalWeight) * Math.random())); const random = BigInt(Math.floor(Number(totalWeight) * Math.random()));
const weightRandomIndex = getWeightRandom(weightsScores, random); const weightRandomIndex = getWeightRandom(weightsScores, random);
return relayers[weightRandomIndex]; return relayers[weightRandomIndex];
} }
export interface RelayerClientConstructor { export interface RelayerClientConstructor {
netId: NetIdType; netId: NetIdType;
config: Config; config: Config;
fetchDataOptions?: fetchDataOptions; fetchDataOptions?: fetchDataOptions;
} }
export class RelayerClient { export class RelayerClient {
netId: NetIdType; netId: NetIdType;
config: Config; config: Config;
selectedRelayer?: RelayerInfo; selectedRelayer?: RelayerInfo;
fetchDataOptions?: fetchDataOptions; fetchDataOptions?: fetchDataOptions;
tovarish: boolean; tovarish: boolean;
constructor({ netId, config, fetchDataOptions }: RelayerClientConstructor) { constructor({ netId, config, fetchDataOptions }: RelayerClientConstructor) {
this.netId = netId; this.netId = netId;
this.config = config; this.config = config;
this.fetchDataOptions = fetchDataOptions; this.fetchDataOptions = fetchDataOptions;
this.tovarish = false; this.tovarish = false;
}
async askRelayerStatus({
hostname,
url,
relayerAddress,
}: {
hostname?: string;
// optional url if entered manually
url?: string;
// relayerAddress from registry contract to prevent cheating
relayerAddress?: string;
}): Promise<RelayerStatus> {
if (!url && hostname) {
url = `https://${!hostname.endsWith('/') ? hostname + '/' : hostname}`;
} else if (url && !url.endsWith('/')) {
url += '/';
} else {
url = '';
} }
const rawStatus = (await fetchData(`${url}status`, { async askRelayerStatus({
...this.fetchDataOptions,
headers: {
'Content-Type': 'application/json, application/x-www-form-urlencoded',
},
timeout: 30000,
maxRetry: this.fetchDataOptions?.torPort ? 2 : 0,
})) as object;
const statusValidator = ajv.compile(getStatusSchema(this.netId, this.config, this.tovarish));
if (!statusValidator(rawStatus)) {
throw new Error('Invalid status schema');
}
const status = {
...rawStatus,
url,
} as RelayerStatus;
if (status.currentQueue > 5) {
throw new Error('Withdrawal queue is overloaded');
}
if (status.netId !== this.netId) {
throw new Error('This relayer serves a different network');
}
if (relayerAddress && this.netId === NetId.MAINNET && status.rewardAccount !== relayerAddress) {
throw new Error('The Relayer reward address must match registered address');
}
return status;
}
async filterRelayer(relayer: CachedRelayerInfo): Promise<RelayerInfo | RelayerError | undefined> {
const hostname = relayer.hostnames[this.netId];
const { ensName, relayerAddress } = relayer;
if (!hostname) {
return;
}
try {
const status = await this.askRelayerStatus({ hostname, relayerAddress });
return {
netId: status.netId,
url: status.url,
hostname, hostname,
ensName, url,
relayerAddress, relayerAddress,
rewardAccount: getAddress(status.rewardAccount), }: {
instances: getSupportedInstances(status.instances), hostname?: string;
stakeBalance: relayer.stakeBalance, // optional url if entered manually
gasPrice: status.gasPrices?.fast, url?: string;
ethPrices: status.ethPrices, // relayerAddress from registry contract to prevent cheating
currentQueue: status.currentQueue, relayerAddress?: string;
tornadoServiceFee: status.tornadoServiceFee, }): Promise<RelayerStatus> {
} as RelayerInfo; if (!url && hostname) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any url = `https://${!hostname.endsWith('/') ? hostname + '/' : hostname}`;
} catch (err: any) { } else if (url && !url.endsWith('/')) {
return { url += '/';
hostname,
relayerAddress,
errorMessage: err.message,
hasError: true,
} as RelayerError;
}
}
async getValidRelayers(relayers: CachedRelayerInfo[]): Promise<{
validRelayers: RelayerInfo[];
invalidRelayers: RelayerError[];
}> {
const invalidRelayers: RelayerError[] = [];
const validRelayers = (await Promise.all(relayers.map((relayer) => this.filterRelayer(relayer)))).filter((r) => {
if (!r) {
return false;
}
if ((r as RelayerError).hasError) {
invalidRelayers.push(r as RelayerError);
return false;
}
return true;
}) as RelayerInfo[];
return {
validRelayers,
invalidRelayers,
};
}
pickWeightedRandomRelayer(relayers: RelayerInfo[]) {
return pickWeightedRandomRelayer(relayers);
}
async tornadoWithdraw(
{ contract, proof, args }: TornadoWithdrawParams,
callback?: (jobResp: RelayerTornadoWithdraw | RelayerTornadoJobs) => void,
) {
const { url } = this.selectedRelayer as RelayerInfo;
/**
* Request new job
*/
const withdrawResponse = (await fetchData(`${url}v1/tornadoWithdraw`, {
...this.fetchDataOptions,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contract,
proof,
args,
}),
})) as RelayerTornadoWithdraw;
const { id, error } = withdrawResponse;
if (error) {
throw new Error(error);
}
const jobValidator = ajv.compile(jobRequestSchema);
if (!jobValidator(withdrawResponse)) {
const errMsg = `${url}v1/tornadoWithdraw has an invalid job response`;
throw new Error(errMsg);
}
if (typeof callback === 'function') {
callback(withdrawResponse as unknown as RelayerTornadoWithdraw);
}
/**
* Get job status
*/
let relayerStatus: string | undefined;
const jobUrl = `${url}v1/jobs/${id}`;
console.log(`Job submitted: ${jobUrl}\n`);
while (!relayerStatus || !['FAILED', 'CONFIRMED'].includes(relayerStatus)) {
const jobResponse = await fetchData(jobUrl, {
...this.fetchDataOptions,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (jobResponse.error) {
throw new Error(error);
}
const jobValidator = ajv.compile(jobsSchema);
if (!jobValidator(jobResponse)) {
const errMsg = `${jobUrl} has an invalid job response`;
throw new Error(errMsg);
}
const { status, txHash, confirmations, failedReason } = jobResponse as unknown as RelayerTornadoJobs;
if (relayerStatus !== status) {
if (status === 'FAILED') {
const errMsg = `Job ${status}: ${jobUrl} failed reason: ${failedReason}`;
throw new Error(errMsg);
} else if (status === 'SENT') {
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}\n`);
} else if (status === 'MINED') {
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`);
} else if (status === 'CONFIRMED') {
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`);
} else { } else {
console.log(`Job ${status}: ${jobUrl}\n`); url = '';
} }
relayerStatus = status; const rawStatus = (await fetchData(`${url}status`, {
...this.fetchDataOptions,
headers: {
'Content-Type':
'application/json, application/x-www-form-urlencoded',
},
timeout: 30000,
maxRetry: this.fetchDataOptions?.torPort ? 2 : 0,
})) as object;
const statusValidator = ajv.compile(
getStatusSchema(this.netId, this.config, this.tovarish),
);
if (!statusValidator(rawStatus)) {
throw new Error('Invalid status schema');
}
const status = {
...rawStatus,
url,
} as RelayerStatus;
if (status.currentQueue > 5) {
throw new Error('Withdrawal queue is overloaded');
}
if (status.netId !== this.netId) {
throw new Error('This relayer serves a different network');
}
if (
relayerAddress &&
this.netId === NetId.MAINNET &&
status.rewardAccount !== relayerAddress
) {
throw new Error(
'The Relayer reward address must match registered address',
);
}
return status;
}
async filterRelayer(
relayer: CachedRelayerInfo,
): Promise<RelayerInfo | RelayerError | undefined> {
const hostname = relayer.hostnames[this.netId];
const { ensName, relayerAddress } = relayer;
if (!hostname) {
return;
}
try {
const status = await this.askRelayerStatus({
hostname,
relayerAddress,
});
return {
netId: status.netId,
url: status.url,
hostname,
ensName,
relayerAddress,
rewardAccount: getAddress(status.rewardAccount),
instances: getSupportedInstances(status.instances),
stakeBalance: relayer.stakeBalance,
gasPrice: status.gasPrices?.fast,
ethPrices: status.ethPrices,
currentQueue: status.currentQueue,
tornadoServiceFee: status.tornadoServiceFee,
} as RelayerInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
return {
hostname,
relayerAddress,
errorMessage: err.message,
hasError: true,
} as RelayerError;
}
}
async getValidRelayers(relayers: CachedRelayerInfo[]): Promise<{
validRelayers: RelayerInfo[];
invalidRelayers: RelayerError[];
}> {
const invalidRelayers: RelayerError[] = [];
const validRelayers = (
await Promise.all(
relayers.map((relayer) => this.filterRelayer(relayer)),
)
).filter((r) => {
if (!r) {
return false;
}
if ((r as RelayerError).hasError) {
invalidRelayers.push(r as RelayerError);
return false;
}
return true;
}) as RelayerInfo[];
return {
validRelayers,
invalidRelayers,
};
}
pickWeightedRandomRelayer(relayers: RelayerInfo[]) {
return pickWeightedRandomRelayer(relayers);
}
async tornadoWithdraw(
{ contract, proof, args }: TornadoWithdrawParams,
callback?: (
jobResp: RelayerTornadoWithdraw | RelayerTornadoJobs,
) => void,
) {
const { url } = this.selectedRelayer as RelayerInfo;
/**
* Request new job
*/
const withdrawResponse = (await fetchData(`${url}v1/tornadoWithdraw`, {
...this.fetchDataOptions,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contract,
proof,
args,
}),
})) as RelayerTornadoWithdraw;
const { id, error } = withdrawResponse;
if (error) {
throw new Error(error);
}
const jobValidator = ajv.compile(jobRequestSchema);
if (!jobValidator(withdrawResponse)) {
const errMsg = `${url}v1/tornadoWithdraw has an invalid job response`;
throw new Error(errMsg);
}
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(jobResponse as unknown as RelayerTornadoJobs); callback(withdrawResponse as unknown as RelayerTornadoWithdraw);
} }
}
await sleep(3000); /**
* Get job status
*/
let relayerStatus: string | undefined;
const jobUrl = `${url}v1/jobs/${id}`;
console.log(`Job submitted: ${jobUrl}\n`);
while (
!relayerStatus ||
!['FAILED', 'CONFIRMED'].includes(relayerStatus)
) {
const jobResponse = await fetchData(jobUrl, {
...this.fetchDataOptions,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (jobResponse.error) {
throw new Error(error);
}
const jobValidator = ajv.compile(jobsSchema);
if (!jobValidator(jobResponse)) {
const errMsg = `${jobUrl} has an invalid job response`;
throw new Error(errMsg);
}
const { status, txHash, confirmations, failedReason } =
jobResponse as unknown as RelayerTornadoJobs;
if (relayerStatus !== status) {
if (status === 'FAILED') {
const errMsg = `Job ${status}: ${jobUrl} failed reason: ${failedReason}`;
throw new Error(errMsg);
} else if (status === 'SENT') {
console.log(
`Job ${status}: ${jobUrl}, txhash: ${txHash}\n`,
);
} else if (status === 'MINED') {
console.log(
`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`,
);
} else if (status === 'CONFIRMED') {
console.log(
`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`,
);
} else {
console.log(`Job ${status}: ${jobUrl}\n`);
}
relayerStatus = status;
if (typeof callback === 'function') {
callback(jobResponse as unknown as RelayerTornadoJobs);
}
}
await sleep(3000);
}
} }
}
} }

View File

@@ -4,28 +4,28 @@ import { BigNumberish, isAddress } from 'ethers';
export const ajv = new Ajv({ allErrors: true }); export const ajv = new Ajv({ allErrors: true });
ajv.addKeyword({ ajv.addKeyword({
keyword: 'BN', keyword: 'BN',
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
validate: (schema: any, data: BigNumberish) => { validate: (schema: any, data: BigNumberish) => {
try { try {
BigInt(data); BigInt(data);
return true; return true;
} catch { } catch {
return false; return false;
} }
}, },
errors: true, errors: true,
}); });
ajv.addKeyword({ ajv.addKeyword({
keyword: 'isAddress', keyword: 'isAddress',
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
validate: (schema: any, data: string) => { validate: (schema: any, data: string) => {
try { try {
return isAddress(data); return isAddress(data);
} catch { } catch {
return false; return false;
} }
}, },
errors: true, errors: true,
}); });

View File

@@ -3,183 +3,214 @@ import { ajv } from './ajv';
import { addressSchemaType, bnSchemaType, bytes32SchemaType } from './types'; import { addressSchemaType, bnSchemaType, bytes32SchemaType } from './types';
const baseEventsSchemaProperty = { const baseEventsSchemaProperty = {
blockNumber: { blockNumber: {
type: 'number', type: 'number',
}, },
logIndex: { logIndex: {
type: 'number', type: 'number',
}, },
transactionHash: bytes32SchemaType, transactionHash: bytes32SchemaType,
} as const; } as const;
const baseEventsSchemaRequired = Object.keys(baseEventsSchemaProperty) as string[]; const baseEventsSchemaRequired = Object.keys(
baseEventsSchemaProperty,
) as string[];
export const governanceEventsSchema = { export const governanceEventsSchema = {
type: 'array', type: 'array',
items: { items: {
anyOf: [ anyOf: [
{ {
type: 'object', type: 'object',
properties: { properties: {
...baseEventsSchemaProperty, ...baseEventsSchemaProperty,
event: { type: 'string' }, event: { type: 'string' },
id: { type: 'number' }, id: { type: 'number' },
proposer: addressSchemaType, proposer: addressSchemaType,
target: addressSchemaType, target: addressSchemaType,
startTime: { type: 'number' }, startTime: { type: 'number' },
endTime: { type: 'number' }, endTime: { type: 'number' },
description: { type: 'string' }, description: { type: 'string' },
}, },
required: [ required: [
...baseEventsSchemaRequired, ...baseEventsSchemaRequired,
'event', 'event',
'id', 'id',
'proposer', 'proposer',
'target', 'target',
'startTime', 'startTime',
'endTime', 'endTime',
'description', 'description',
],
additionalProperties: false,
},
{
type: 'object',
properties: {
...baseEventsSchemaProperty,
event: { type: 'string' },
proposalId: { type: 'number' },
voter: addressSchemaType,
support: { type: 'boolean' },
votes: { type: 'string' },
from: addressSchemaType,
input: { type: 'string' },
},
required: [
...baseEventsSchemaRequired,
'event',
'proposalId',
'voter',
'support',
'votes',
'from',
'input',
],
additionalProperties: false,
},
{
type: 'object',
properties: {
...baseEventsSchemaProperty,
event: { type: 'string' },
account: addressSchemaType,
delegateTo: addressSchemaType,
},
required: [
...baseEventsSchemaRequired,
'account',
'delegateTo',
],
additionalProperties: false,
},
{
type: 'object',
properties: {
...baseEventsSchemaProperty,
event: { type: 'string' },
account: addressSchemaType,
delegateFrom: addressSchemaType,
},
required: [
...baseEventsSchemaRequired,
'account',
'delegateFrom',
],
additionalProperties: false,
},
], ],
additionalProperties: false, },
},
{
type: 'object',
properties: {
...baseEventsSchemaProperty,
event: { type: 'string' },
proposalId: { type: 'number' },
voter: addressSchemaType,
support: { type: 'boolean' },
votes: { type: 'string' },
from: addressSchemaType,
input: { type: 'string' },
},
required: [...baseEventsSchemaRequired, 'event', 'proposalId', 'voter', 'support', 'votes', 'from', 'input'],
additionalProperties: false,
},
{
type: 'object',
properties: {
...baseEventsSchemaProperty,
event: { type: 'string' },
account: addressSchemaType,
delegateTo: addressSchemaType,
},
required: [...baseEventsSchemaRequired, 'account', 'delegateTo'],
additionalProperties: false,
},
{
type: 'object',
properties: {
...baseEventsSchemaProperty,
event: { type: 'string' },
account: addressSchemaType,
delegateFrom: addressSchemaType,
},
required: [...baseEventsSchemaRequired, 'account', 'delegateFrom'],
additionalProperties: false,
},
],
},
} as const; } as const;
export const registeredEventsSchema = { export const registeredEventsSchema = {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
...baseEventsSchemaProperty, ...baseEventsSchemaProperty,
ensName: { type: 'string' }, ensName: { type: 'string' },
relayerAddress: addressSchemaType, relayerAddress: addressSchemaType,
},
required: [...baseEventsSchemaRequired, 'ensName', 'relayerAddress'],
additionalProperties: false,
}, },
required: [...baseEventsSchemaRequired, 'ensName', 'relayerAddress'],
additionalProperties: false,
},
} as const; } as const;
export const depositsEventsSchema = { export const depositsEventsSchema = {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
...baseEventsSchemaProperty, ...baseEventsSchemaProperty,
commitment: bytes32SchemaType, commitment: bytes32SchemaType,
leafIndex: { type: 'number' }, leafIndex: { type: 'number' },
timestamp: { type: 'number' }, timestamp: { type: 'number' },
from: addressSchemaType, from: addressSchemaType,
},
required: [
...baseEventsSchemaRequired,
'commitment',
'leafIndex',
'timestamp',
'from',
],
additionalProperties: false,
}, },
required: [...baseEventsSchemaRequired, 'commitment', 'leafIndex', 'timestamp', 'from'],
additionalProperties: false,
},
} as const; } as const;
export const withdrawalsEventsSchema = { export const withdrawalsEventsSchema = {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
...baseEventsSchemaProperty, ...baseEventsSchemaProperty,
nullifierHash: bytes32SchemaType, nullifierHash: bytes32SchemaType,
to: addressSchemaType, to: addressSchemaType,
fee: bnSchemaType, fee: bnSchemaType,
timestamp: { type: 'number' }, timestamp: { type: 'number' },
},
required: [
...baseEventsSchemaRequired,
'nullifierHash',
'to',
'fee',
'timestamp',
],
additionalProperties: false,
}, },
required: [...baseEventsSchemaRequired, 'nullifierHash', 'to', 'fee', 'timestamp'],
additionalProperties: false,
},
} as const; } as const;
export const echoEventsSchema = { export const echoEventsSchema = {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
...baseEventsSchemaProperty, ...baseEventsSchemaProperty,
address: addressSchemaType, address: addressSchemaType,
encryptedAccount: { type: 'string' }, encryptedAccount: { type: 'string' },
},
required: [...baseEventsSchemaRequired, 'address', 'encryptedAccount'],
additionalProperties: false,
}, },
required: [...baseEventsSchemaRequired, 'address', 'encryptedAccount'],
additionalProperties: false,
},
} as const; } as const;
export const encryptedNotesSchema = { export const encryptedNotesSchema = {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
...baseEventsSchemaProperty, ...baseEventsSchemaProperty,
encryptedNote: { type: 'string' }, encryptedNote: { type: 'string' },
},
required: [...baseEventsSchemaRequired, 'encryptedNote'],
additionalProperties: false,
}, },
required: [...baseEventsSchemaRequired, 'encryptedNote'],
additionalProperties: false,
},
} as const; } as const;
export function getEventsSchemaValidator(type: string) { export function getEventsSchemaValidator(type: string) {
if (type === DEPOSIT) { if (type === DEPOSIT) {
return ajv.compile(depositsEventsSchema); return ajv.compile(depositsEventsSchema);
} }
if (type === WITHDRAWAL) { if (type === WITHDRAWAL) {
return ajv.compile(withdrawalsEventsSchema); return ajv.compile(withdrawalsEventsSchema);
} }
if (type === 'governance') { if (type === 'governance') {
return ajv.compile(governanceEventsSchema); return ajv.compile(governanceEventsSchema);
} }
if (type === 'registered') { if (type === 'registered') {
return ajv.compile(registeredEventsSchema); return ajv.compile(registeredEventsSchema);
} }
if (type === 'echo') { if (type === 'echo') {
return ajv.compile(echoEventsSchema); return ajv.compile(echoEventsSchema);
} }
if (type === 'encrypted_notes') { if (type === 'encrypted_notes') {
return ajv.compile(encryptedNotesSchema); return ajv.compile(encryptedNotesSchema);
} }
throw new Error('Unsupported event type for schema validation'); throw new Error('Unsupported event type for schema validation');
} }

View File

@@ -1,64 +1,64 @@
export interface jobsSchema { export interface jobsSchema {
type: string; type: string;
properties: { properties: {
error: { error: {
type: string; type: string;
};
id: {
type: string;
};
type: {
type: string;
};
status: {
type: string;
};
contract: {
type: string;
};
proof: {
type: string;
};
args: {
type: string;
items: {
type: string;
};
};
txHash: {
type: string;
};
confirmations: {
type: string;
};
failedReason: {
type: string;
};
}; };
id: { required: string[];
type: string;
};
type: {
type: string;
};
status: {
type: string;
};
contract: {
type: string;
};
proof: {
type: string;
};
args: {
type: string;
items: {
type: string;
};
};
txHash: {
type: string;
};
confirmations: {
type: string;
};
failedReason: {
type: string;
};
};
required: string[];
} }
export const jobsSchema: jobsSchema = { export const jobsSchema: jobsSchema = {
type: 'object', type: 'object',
properties: { properties: {
error: { type: 'string' }, error: { type: 'string' },
id: { type: 'string' }, id: { type: 'string' },
type: { type: 'string' }, type: { type: 'string' },
status: { type: 'string' }, status: { type: 'string' },
contract: { type: 'string' }, contract: { type: 'string' },
proof: { type: 'string' }, proof: { type: 'string' },
args: { args: {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
},
txHash: { type: 'string' },
confirmations: { type: 'number' },
failedReason: { type: 'string' },
}, },
txHash: { type: 'string' }, required: ['id', 'status'],
confirmations: { type: 'number' },
failedReason: { type: 'string' },
},
required: ['id', 'status'],
}; };
export const jobRequestSchema: jobsSchema = { export const jobRequestSchema: jobsSchema = {
...jobsSchema, ...jobsSchema,
required: ['id'], required: ['id'],
}; };

View File

@@ -2,212 +2,258 @@ import { Config, NetId, NetIdType } from '../networkConfig';
import { addressSchemaType, bnSchemaType } from '.'; import { addressSchemaType, bnSchemaType } from '.';
export interface statusInstanceType { export interface statusInstanceType {
type: string; type: string;
properties: { properties: {
instanceAddress: { instanceAddress: {
type: string; type: string;
properties: { properties: {
[key in string]: typeof addressSchemaType; [key in string]: typeof addressSchemaType;
}; };
required: string[]; required: string[];
};
tokenAddress?: typeof addressSchemaType;
symbol?: { enum: string[] };
decimals: { enum: number[] };
}; };
tokenAddress?: typeof addressSchemaType; required: string[];
symbol?: { enum: string[] };
decimals: { enum: number[] };
};
required: string[];
} }
export interface statusInstancesType { export interface statusInstancesType {
type: string; type: string;
properties: { properties: {
[key in string]: statusInstanceType; [key in string]: statusInstanceType;
}; };
required: string[]; required: string[];
} }
export interface statusEthPricesType { export interface statusEthPricesType {
type: string; type: string;
properties: { properties: {
[key in string]: typeof bnSchemaType; [key in string]: typeof bnSchemaType;
}; };
required?: string[]; required?: string[];
} }
export interface statusSchema { export interface statusSchema {
type: string; type: string;
properties: { properties: {
rewardAccount: typeof addressSchemaType; rewardAccount: typeof addressSchemaType;
instances?: statusInstancesType; instances?: statusInstancesType;
gasPrices: { gasPrices: {
type: string; type: string;
properties: { properties: {
[key in string]: { [key in string]: {
type: string; type: string;
};
};
required: string[];
};
netId: {
type: string;
};
ethPrices?: statusEthPricesType;
tornadoServiceFee?: {
type: string;
maximum: number;
minimum: number;
};
latestBlock: {
type: string;
};
latestBalance: {
type: string;
BN: boolean;
};
version: {
type: string;
};
health: {
type: string;
properties: {
status: { const: string };
error: { type: string };
};
required: string[];
};
syncStatus: {
type: string;
properties: {
events: { type: string };
tokenPrice: { type: string };
gasPrice: { type: string };
};
required: string[];
};
onSyncEvents: { type: string };
currentQueue: {
type: string;
}; };
};
required: string[];
}; };
netId: { required: string[];
type: string;
};
ethPrices?: statusEthPricesType;
tornadoServiceFee?: {
type: string;
maximum: number;
minimum: number;
};
latestBlock: {
type: string;
};
latestBalance: {
type: string;
BN: boolean;
};
version: {
type: string;
};
health: {
type: string;
properties: {
status: { const: string };
error: { type: string };
};
required: string[];
};
syncStatus: {
type: string;
properties: {
events: { type: string };
tokenPrice: { type: string };
gasPrice: { type: string };
};
required: string[];
};
onSyncEvents: { type: string };
currentQueue: {
type: string;
};
};
required: string[];
} }
const statusSchema: statusSchema = { const statusSchema: statusSchema = {
type: 'object', type: 'object',
properties: { properties: {
rewardAccount: addressSchemaType, rewardAccount: addressSchemaType,
gasPrices: { gasPrices: {
type: 'object', type: 'object',
properties: { properties: {
fast: { type: 'number' }, fast: { type: 'number' },
additionalProperties: { type: 'number' }, additionalProperties: { type: 'number' },
}, },
required: ['fast'], required: ['fast'],
},
netId: { type: 'integer' },
tornadoServiceFee: { type: 'number', maximum: 20, minimum: 0 },
latestBlock: { type: 'number' },
latestBalance: bnSchemaType,
version: { type: 'string' },
health: {
type: 'object',
properties: {
status: { const: 'true' },
error: { type: 'string' },
},
required: ['status'],
},
syncStatus: {
type: 'object',
properties: {
events: { type: 'boolean' },
tokenPrice: { type: 'boolean' },
gasPrice: { type: 'boolean' },
},
required: ['events', 'tokenPrice', 'gasPrice'],
},
onSyncEvents: { type: 'boolean' },
currentQueue: { type: 'number' },
}, },
netId: { type: 'integer' }, required: [
tornadoServiceFee: { type: 'number', maximum: 20, minimum: 0 }, 'rewardAccount',
latestBlock: { type: 'number' }, 'instances',
latestBalance: bnSchemaType, 'netId',
version: { type: 'string' }, 'tornadoServiceFee',
health: { 'version',
type: 'object', 'health',
properties: { 'currentQueue',
status: { const: 'true' }, ],
error: { type: 'string' },
},
required: ['status'],
},
syncStatus: {
type: 'object',
properties: {
events: { type: 'boolean' },
tokenPrice: { type: 'boolean' },
gasPrice: { type: 'boolean' },
},
required: ['events', 'tokenPrice', 'gasPrice'],
},
onSyncEvents: { type: 'boolean' },
currentQueue: { type: 'number' },
},
required: ['rewardAccount', 'instances', 'netId', 'tornadoServiceFee', 'version', 'health', 'currentQueue'],
}; };
export function getStatusSchema(netId: NetIdType, config: Config, tovarish: boolean) { export function getStatusSchema(
const { tokens, optionalTokens, disabledTokens, nativeCurrency } = config; netId: NetIdType,
config: Config,
tovarish: boolean,
) {
const { tokens, optionalTokens, disabledTokens, nativeCurrency } = config;
// deep copy schema // deep copy schema
const schema = JSON.parse(JSON.stringify(statusSchema)) as statusSchema; const schema = JSON.parse(JSON.stringify(statusSchema)) as statusSchema;
const instances = Object.keys(tokens).reduce( const instances = Object.keys(tokens).reduce(
(acc: statusInstancesType, token) => { (acc: statusInstancesType, token) => {
const { instanceAddress, tokenAddress, symbol, decimals, optionalInstances = [] } = tokens[token]; const {
const amounts = Object.keys(instanceAddress); instanceAddress,
tokenAddress,
symbol,
decimals,
optionalInstances = [],
} = tokens[token];
const amounts = Object.keys(instanceAddress);
const instanceProperties: statusInstanceType = { const instanceProperties: statusInstanceType = {
type: 'object', type: 'object',
properties: { properties: {
instanceAddress: { instanceAddress: {
type: 'object', type: 'object',
properties: amounts.reduce((acc: { [key in string]: typeof addressSchemaType }, cur) => { properties: amounts.reduce(
acc[cur] = addressSchemaType; (
return acc; acc: {
}, {}), [key in string]: typeof addressSchemaType;
required: amounts.filter((amount) => !optionalInstances.includes(amount)), },
}, cur,
decimals: { enum: [decimals] }, ) => {
acc[cur] = addressSchemaType;
return acc;
},
{},
),
required: amounts.filter(
(amount) => !optionalInstances.includes(amount),
),
},
decimals: { enum: [decimals] },
},
required: ['instanceAddress', 'decimals'].concat(
tokenAddress ? ['tokenAddress'] : [],
symbol ? ['symbol'] : [],
),
};
if (tokenAddress) {
instanceProperties.properties.tokenAddress = addressSchemaType;
}
if (symbol) {
instanceProperties.properties.symbol = { enum: [symbol] };
}
acc.properties[token] = instanceProperties;
if (
!optionalTokens?.includes(token) &&
!disabledTokens?.includes(token)
) {
acc.required.push(token);
}
return acc;
}, },
required: ['instanceAddress', 'decimals'].concat( {
tokenAddress ? ['tokenAddress'] : [], type: 'object',
symbol ? ['symbol'] : [], properties: {},
), required: [],
}; },
);
if (tokenAddress) { schema.properties.instances = instances;
instanceProperties.properties.tokenAddress = addressSchemaType;
}
if (symbol) {
instanceProperties.properties.symbol = { enum: [symbol] };
}
acc.properties[token] = instanceProperties; const _tokens = Object.keys(tokens).filter(
if (!optionalTokens?.includes(token) && !disabledTokens?.includes(token)) { (t) =>
acc.required.push(token); t !== nativeCurrency &&
} !config.optionalTokens?.includes(t) &&
return acc; !config.disabledTokens?.includes(t),
}, );
{
type: 'object',
properties: {},
required: [],
},
);
schema.properties.instances = instances; if (netId === NetId.MAINNET) {
_tokens.push('torn');
}
const _tokens = Object.keys(tokens).filter( if (_tokens.length) {
(t) => t !== nativeCurrency && !config.optionalTokens?.includes(t) && !config.disabledTokens?.includes(t), const ethPrices: statusEthPricesType = {
); type: 'object',
properties: _tokens.reduce(
(
acc: { [key in string]: typeof bnSchemaType },
token: string,
) => {
acc[token] = bnSchemaType;
return acc;
},
{},
),
required: _tokens,
};
schema.properties.ethPrices = ethPrices;
schema.required.push('ethPrices');
}
if (netId === NetId.MAINNET) { if (tovarish) {
_tokens.push('torn'); schema.required.push(
} 'gasPrices',
'latestBlock',
'latestBalance',
'syncStatus',
'onSyncEvents',
);
}
if (_tokens.length) { return schema;
const ethPrices: statusEthPricesType = {
type: 'object',
properties: _tokens.reduce((acc: { [key in string]: typeof bnSchemaType }, token: string) => {
acc[token] = bnSchemaType;
return acc;
}, {}),
required: _tokens,
};
schema.properties.ethPrices = ethPrices;
schema.required.push('ethPrices');
}
if (tovarish) {
schema.required.push('gasPrices', 'latestBlock', 'latestBalance', 'syncStatus', 'onSyncEvents');
}
return schema;
} }

View File

@@ -1,9 +1,15 @@
export const addressSchemaType = { export const addressSchemaType = {
type: 'string', type: 'string',
pattern: '^0x[a-fA-F0-9]{40}$', pattern: '^0x[a-fA-F0-9]{40}$',
isAddress: true, isAddress: true,
} as const; } as const;
export const bnSchemaType = { type: 'string', BN: true } as const; export const bnSchemaType = { type: 'string', BN: true } as const;
export const proofSchemaType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' } as const; export const proofSchemaType = {
export const bytes32SchemaType = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' } as const; type: 'string',
pattern: '^0x[a-fA-F0-9]{512}$',
} as const;
export const bytes32SchemaType = {
type: 'string',
pattern: '^0x[a-fA-F0-9]{64}$',
} as const;
export const bytes32BNSchemaType = { ...bytes32SchemaType, BN: true } as const; export const bytes32BNSchemaType = { ...bytes32SchemaType, BN: true } as const;

View File

@@ -4,87 +4,91 @@ import { chunk } from './utils';
import { Call3, multicall } from './multicall'; import { Call3, multicall } from './multicall';
export interface tokenBalances { export interface tokenBalances {
address: string; address: string;
name: string; name: string;
symbol: string; symbol: string;
decimals: number; decimals: number;
balance: bigint; balance: bigint;
} }
export async function getTokenBalances({ export async function getTokenBalances({
provider, provider,
Multicall, Multicall,
currencyName, currencyName,
userAddress, userAddress,
tokenAddresses = [], tokenAddresses = [],
}: { }: {
provider: Provider; provider: Provider;
Multicall: Multicall; Multicall: Multicall;
currencyName: string; currencyName: string;
userAddress: string; userAddress: string;
tokenAddresses: string[]; tokenAddresses: string[];
}): Promise<tokenBalances[]> { }): Promise<tokenBalances[]> {
const tokenCalls = tokenAddresses const tokenCalls = tokenAddresses
.map((tokenAddress) => { .map((tokenAddress) => {
const Token = ERC20__factory.connect(tokenAddress, provider); const Token = ERC20__factory.connect(tokenAddress, provider);
return [ return [
{
contract: Token,
name: 'balanceOf',
params: [userAddress],
},
{
contract: Token,
name: 'name',
},
{
contract: Token,
name: 'symbol',
},
{
contract: Token,
name: 'decimals',
},
];
})
.flat() as Call3[];
const multicallResults = await multicall(Multicall, [
{ {
contract: Token, contract: Multicall,
name: 'balanceOf', name: 'getEthBalance',
params: [userAddress], params: [userAddress],
}, },
...(tokenCalls.length ? tokenCalls : []),
]);
const ethResults = multicallResults[0];
const tokenResults = multicallResults.slice(1).length
? chunk(
multicallResults.slice(1),
tokenCalls.length / tokenAddresses.length,
)
: [];
const tokenBalances = tokenResults.map((tokenResult, index) => {
const [tokenBalance, tokenName, tokenSymbol, tokenDecimals] =
tokenResult;
const tokenAddress = tokenAddresses[index];
return {
address: tokenAddress,
name: tokenName,
symbol: tokenSymbol,
decimals: Number(tokenDecimals),
balance: tokenBalance,
};
});
return [
{ {
contract: Token, address: ZeroAddress,
name: 'name', name: currencyName,
symbol: currencyName,
decimals: 18,
balance: ethResults,
}, },
{ ...tokenBalances,
contract: Token, ];
name: 'symbol',
},
{
contract: Token,
name: 'decimals',
},
];
})
.flat() as Call3[];
const multicallResults = await multicall(Multicall, [
{
contract: Multicall,
name: 'getEthBalance',
params: [userAddress],
},
...(tokenCalls.length ? tokenCalls : []),
]);
const ethResults = multicallResults[0];
const tokenResults = multicallResults.slice(1).length
? chunk(multicallResults.slice(1), tokenCalls.length / tokenAddresses.length)
: [];
const tokenBalances = tokenResults.map((tokenResult, index) => {
const [tokenBalance, tokenName, tokenSymbol, tokenDecimals] = tokenResult;
const tokenAddress = tokenAddresses[index];
return {
address: tokenAddress,
name: tokenName,
symbol: tokenSymbol,
decimals: Number(tokenDecimals),
balance: tokenBalance,
};
});
return [
{
address: ZeroAddress,
name: currencyName,
symbol: currencyName,
decimals: 18,
balance: ethResults,
},
...tokenBalances,
];
} }

View File

@@ -1,12 +1,12 @@
import { getAddress } from 'ethers'; import { getAddress } from 'ethers';
import { import {
RelayerClient, RelayerClient,
RelayerClientConstructor, RelayerClientConstructor,
RelayerError, RelayerError,
RelayerInfo, RelayerInfo,
RelayerStatus, RelayerStatus,
getSupportedInstances, getSupportedInstances,
} from './relayerClient'; } from './relayerClient';
import { fetchData } from './providers'; import { fetchData } from './providers';
import { CachedRelayerInfo, MinimalEvents } from './events'; import { CachedRelayerInfo, MinimalEvents } from './events';
@@ -17,262 +17,213 @@ import { enabledChains, getConfig, NetId, NetIdType } from './networkConfig';
export const MAX_TOVARISH_EVENTS = 5000; export const MAX_TOVARISH_EVENTS = 5000;
export interface EventsStatus { export interface EventsStatus {
events: number; events: number;
lastBlock: number; lastBlock: number;
} }
export interface InstanceEventsStatus { export interface InstanceEventsStatus {
[index: string]: { [index: string]: {
deposits: EventsStatus; deposits: EventsStatus;
withdrawals: EventsStatus; withdrawals: EventsStatus;
}; };
} }
export interface CurrencyEventsStatus { export interface CurrencyEventsStatus {
[index: string]: InstanceEventsStatus; [index: string]: InstanceEventsStatus;
} }
export interface TovarishEventsStatus { export interface TovarishEventsStatus {
governance?: EventsStatus; governance?: EventsStatus;
registered?: { registered?: {
lastBlock: number; lastBlock: number;
timestamp: number; timestamp: number;
relayers: number; relayers: number;
}; };
echo: EventsStatus; echo: EventsStatus;
encrypted_notes: EventsStatus; encrypted_notes: EventsStatus;
instances: CurrencyEventsStatus; instances: CurrencyEventsStatus;
} }
export interface TovarishSyncStatus { export interface TovarishSyncStatus {
events: boolean; events: boolean;
tokenPrice: boolean; tokenPrice: boolean;
gasPrice: boolean; gasPrice: boolean;
} }
// Expected response from /status endpoint // Expected response from /status endpoint
export interface TovarishStatus extends RelayerStatus { export interface TovarishStatus extends RelayerStatus {
latestBalance: string; latestBalance: string;
events: TovarishEventsStatus; events: TovarishEventsStatus;
syncStatus: TovarishSyncStatus; syncStatus: TovarishSyncStatus;
onSyncEvents: boolean; onSyncEvents: boolean;
} }
// Formatted TovarishStatus for Frontend usage // Formatted TovarishStatus for Frontend usage
export interface TovarishInfo extends RelayerInfo { export interface TovarishInfo extends RelayerInfo {
latestBlock: number; latestBlock: number;
latestBalance: string; latestBalance: string;
version: string; version: string;
events: TovarishEventsStatus; events: TovarishEventsStatus;
syncStatus: TovarishSyncStatus; syncStatus: TovarishSyncStatus;
} }
// Query input for TovarishEvents // Query input for TovarishEvents
export interface TovarishEventsQuery { export interface TovarishEventsQuery {
type: string; type: string;
currency?: string; currency?: string;
amount?: string; amount?: string;
fromBlock: number; fromBlock: number;
recent?: boolean; recent?: boolean;
} }
export interface BaseTovarishEvents<T> { export interface BaseTovarishEvents<T> {
events: T[]; events: T[];
lastSyncBlock: number; lastSyncBlock: number;
} }
export class TovarishClient extends RelayerClient { export class TovarishClient extends RelayerClient {
declare selectedRelayer?: TovarishInfo; declare selectedRelayer?: TovarishInfo;
constructor(clientConstructor: RelayerClientConstructor) { constructor(clientConstructor: RelayerClientConstructor) {
super(clientConstructor); super(clientConstructor);
this.tovarish = true; this.tovarish = true;
}
async askRelayerStatus({
hostname,
url,
relayerAddress,
}: {
hostname?: string;
// optional url if entered manually
url?: string;
// relayerAddress from registry contract to prevent cheating
relayerAddress?: string;
}): Promise<TovarishStatus> {
const status = (await super.askRelayerStatus({ hostname, url, relayerAddress })) as TovarishStatus;
if (!status.version.includes('tovarish')) {
throw new Error('Not a tovarish relayer!');
} }
return status; async askRelayerStatus({
}
/**
* Ask status for all enabled chains for tovarish relayer
*/
async askAllStatus({
hostname,
url,
relayerAddress,
}: {
hostname?: string;
// optional url if entered manually
url?: string;
// relayerAddress from registry contract to prevent cheating
relayerAddress?: string;
}): Promise<TovarishStatus[]> {
if (!url && hostname) {
url = `https://${!hostname.endsWith('/') ? hostname + '/' : hostname}`;
} else if (url && !url.endsWith('/')) {
url += '/';
} else {
url = '';
}
const statusArray = (await fetchData(`${url}status`, {
...this.fetchDataOptions,
headers: {
'Content-Type': 'application/json, application/x-www-form-urlencoded',
},
timeout: 30000,
maxRetry: this.fetchDataOptions?.torPort ? 2 : 0,
})) as object;
if (!Array.isArray(statusArray)) {
return [];
}
const tovarishStatus: TovarishStatus[] = [];
for (const rawStatus of statusArray) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const netId = (rawStatus as any).netId as NetIdType;
const config = getConfig(netId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusValidator = ajv.compile(getStatusSchema((rawStatus as any).netId, config, this.tovarish));
if (!statusValidator) {
continue;
}
const status = {
...rawStatus,
url: `${url}${netId}/`,
} as TovarishStatus;
if (status.currentQueue > 5) {
throw new Error('Withdrawal queue is overloaded');
}
if (!enabledChains.includes(status.netId)) {
throw new Error('This relayer serves a different network');
}
if (relayerAddress && status.netId === NetId.MAINNET && status.rewardAccount !== relayerAddress) {
throw new Error('The Relayer reward address must match registered address');
}
if (!status.version.includes('tovarish')) {
throw new Error('Not a tovarish relayer!');
}
tovarishStatus.push(status);
}
return tovarishStatus;
}
async filterRelayer(relayer: CachedRelayerInfo): Promise<TovarishInfo | RelayerError | undefined> {
const { ensName, relayerAddress, tovarishHost, tovarishNetworks } = relayer;
if (!tovarishHost || !tovarishNetworks?.includes(this.netId)) {
return;
}
const hostname = `${tovarishHost}/${this.netId}`;
try {
const status = await this.askRelayerStatus({ hostname, relayerAddress });
return {
netId: status.netId,
url: status.url,
hostname, hostname,
ensName, url,
relayerAddress, relayerAddress,
rewardAccount: getAddress(status.rewardAccount), }: {
instances: getSupportedInstances(status.instances), hostname?: string;
stakeBalance: relayer.stakeBalance, // optional url if entered manually
gasPrice: status.gasPrices?.fast, url?: string;
ethPrices: status.ethPrices, // relayerAddress from registry contract to prevent cheating
currentQueue: status.currentQueue, relayerAddress?: string;
tornadoServiceFee: status.tornadoServiceFee, }): Promise<TovarishStatus> {
// Additional fields for tovarish relayer const status = (await super.askRelayerStatus({
latestBlock: Number(status.latestBlock), hostname,
latestBalance: status.latestBalance, url,
version: status.version, relayerAddress,
events: status.events, })) as TovarishStatus;
syncStatus: status.syncStatus,
} as TovarishInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (!status.version.includes('tovarish')) {
} catch (err: any) { throw new Error('Not a tovarish relayer!');
return { }
hostname,
relayerAddress, return status;
errorMessage: err.message,
hasError: true,
} as RelayerError;
} }
}
async getValidRelayers(relayers: CachedRelayerInfo[]): Promise<{ /**
validRelayers: TovarishInfo[]; * Ask status for all enabled chains for tovarish relayer
invalidRelayers: RelayerError[]; */
}> { async askAllStatus({
const invalidRelayers: RelayerError[] = []; hostname,
url,
relayerAddress,
}: {
hostname?: string;
// optional url if entered manually
url?: string;
// relayerAddress from registry contract to prevent cheating
relayerAddress?: string;
}): Promise<TovarishStatus[]> {
if (!url && hostname) {
url = `https://${!hostname.endsWith('/') ? hostname + '/' : hostname}`;
} else if (url && !url.endsWith('/')) {
url += '/';
} else {
url = '';
}
const validRelayers = (await Promise.all(relayers.map((relayer) => this.filterRelayer(relayer)))).filter((r) => { const statusArray = (await fetchData(`${url}status`, {
if (!r) { ...this.fetchDataOptions,
return false; headers: {
} 'Content-Type':
if ((r as RelayerError).hasError) { 'application/json, application/x-www-form-urlencoded',
invalidRelayers.push(r as RelayerError); },
return false; timeout: 30000,
} maxRetry: this.fetchDataOptions?.torPort ? 2 : 0,
return true; })) as object;
}) as TovarishInfo[];
return { if (!Array.isArray(statusArray)) {
validRelayers, return [];
invalidRelayers, }
};
}
async getTovarishRelayers(relayers: CachedRelayerInfo[]): Promise<{ const tovarishStatus: TovarishStatus[] = [];
validRelayers: TovarishInfo[];
invalidRelayers: RelayerError[];
}> {
const validRelayers: TovarishInfo[] = [];
const invalidRelayers: RelayerError[] = [];
await Promise.all( for (const rawStatus of statusArray) {
relayers // eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((r) => r.tovarishHost && r.tovarishNetworks?.length) const netId = (rawStatus as any).netId as NetIdType;
.map(async (relayer) => { const config = getConfig(netId);
const { ensName, relayerAddress, tovarishHost } = relayer;
try { const statusValidator = ajv.compile(
const statusArray = await this.askAllStatus({ hostname: tovarishHost as string, relayerAddress }); getStatusSchema(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(rawStatus as any).netId,
config,
this.tovarish,
),
);
for (const status of statusArray) { if (!statusValidator) {
validRelayers.push({ continue;
}
const status = {
...rawStatus,
url: `${url}${netId}/`,
} as TovarishStatus;
if (status.currentQueue > 5) {
throw new Error('Withdrawal queue is overloaded');
}
if (!enabledChains.includes(status.netId)) {
throw new Error('This relayer serves a different network');
}
if (
relayerAddress &&
status.netId === NetId.MAINNET &&
status.rewardAccount !== relayerAddress
) {
throw new Error(
'The Relayer reward address must match registered address',
);
}
if (!status.version.includes('tovarish')) {
throw new Error('Not a tovarish relayer!');
}
tovarishStatus.push(status);
}
return tovarishStatus;
}
async filterRelayer(
relayer: CachedRelayerInfo,
): Promise<TovarishInfo | RelayerError | undefined> {
const { ensName, relayerAddress, tovarishHost, tovarishNetworks } =
relayer;
if (!tovarishHost || !tovarishNetworks?.includes(this.netId)) {
return;
}
const hostname = `${tovarishHost}/${this.netId}`;
try {
const status = await this.askRelayerStatus({
hostname,
relayerAddress,
});
return {
netId: status.netId, netId: status.netId,
url: status.url, url: status.url,
hostname: tovarishHost as string, hostname,
ensName, ensName,
relayerAddress, relayerAddress,
rewardAccount: getAddress(status.rewardAccount), rewardAccount: getAddress(status.rewardAccount),
@@ -288,108 +239,192 @@ export class TovarishClient extends RelayerClient {
version: status.version, version: status.version,
events: status.events, events: status.events,
syncStatus: status.syncStatus, syncStatus: status.syncStatus,
}); } as TovarishInfo;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
invalidRelayers.push({ return {
hostname: tovarishHost as string, hostname,
relayerAddress, relayerAddress,
errorMessage: err.message, errorMessage: err.message,
hasError: true, hasError: true,
}); } as RelayerError;
} }
}), }
);
async getValidRelayers(relayers: CachedRelayerInfo[]): Promise<{
return { validRelayers: TovarishInfo[];
validRelayers, invalidRelayers: RelayerError[];
invalidRelayers, }> {
}; const invalidRelayers: RelayerError[] = [];
}
const validRelayers = (
async getEvents<T extends MinimalEvents>({ await Promise.all(
type, relayers.map((relayer) => this.filterRelayer(relayer)),
currency, )
amount, ).filter((r) => {
fromBlock, if (!r) {
recent, return false;
}: TovarishEventsQuery): Promise<BaseTovarishEvents<T>> { }
const url = `${this.selectedRelayer?.url}events`; if ((r as RelayerError).hasError) {
invalidRelayers.push(r as RelayerError);
const schemaValidator = getEventsSchemaValidator(type); return false;
}
try { return true;
const events = []; }) as TovarishInfo[];
let lastSyncBlock = fromBlock;
return {
// eslint-disable-next-line no-constant-condition validRelayers,
while (true) { invalidRelayers,
// eslint-disable-next-line prefer-const };
let { events: fetchedEvents, lastSyncBlock: currentBlock } = (await fetchData(url, { }
...this.fetchDataOptions,
method: 'POST', async getTovarishRelayers(relayers: CachedRelayerInfo[]): Promise<{
headers: { validRelayers: TovarishInfo[];
'Content-Type': 'application/json', invalidRelayers: RelayerError[];
}, }> {
body: JSON.stringify({ const validRelayers: TovarishInfo[] = [];
type, const invalidRelayers: RelayerError[] = [];
currency,
amount, await Promise.all(
fromBlock, relayers
recent, .filter((r) => r.tovarishHost && r.tovarishNetworks?.length)
}), .map(async (relayer) => {
})) as BaseTovarishEvents<T>; const { ensName, relayerAddress, tovarishHost } = relayer;
if (!schemaValidator(fetchedEvents)) { try {
const errMsg = `Schema validation failed for ${type} events`; const statusArray = await this.askAllStatus({
throw new Error(errMsg); hostname: tovarishHost as string,
relayerAddress,
});
for (const status of statusArray) {
validRelayers.push({
netId: status.netId,
url: status.url,
hostname: tovarishHost as string,
ensName,
relayerAddress,
rewardAccount: getAddress(status.rewardAccount),
instances: getSupportedInstances(
status.instances,
),
stakeBalance: relayer.stakeBalance,
gasPrice: status.gasPrices?.fast,
ethPrices: status.ethPrices,
currentQueue: status.currentQueue,
tornadoServiceFee: status.tornadoServiceFee,
// Additional fields for tovarish relayer
latestBlock: Number(status.latestBlock),
latestBalance: status.latestBalance,
version: status.version,
events: status.events,
syncStatus: status.syncStatus,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
invalidRelayers.push({
hostname: tovarishHost as string,
relayerAddress,
errorMessage: err.message,
hasError: true,
});
}
}),
);
return {
validRelayers,
invalidRelayers,
};
}
async getEvents<T extends MinimalEvents>({
type,
currency,
amount,
fromBlock,
recent,
}: TovarishEventsQuery): Promise<BaseTovarishEvents<T>> {
const url = `${this.selectedRelayer?.url}events`;
const schemaValidator = getEventsSchemaValidator(type);
try {
const events = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line prefer-const
let { events: fetchedEvents, lastSyncBlock: currentBlock } =
(await fetchData(url, {
...this.fetchDataOptions,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type,
currency,
amount,
fromBlock,
recent,
}),
})) as BaseTovarishEvents<T>;
if (!schemaValidator(fetchedEvents)) {
const errMsg = `Schema validation failed for ${type} events`;
throw new Error(errMsg);
}
if (recent) {
return {
events: fetchedEvents,
lastSyncBlock: currentBlock,
};
}
lastSyncBlock = currentBlock;
if (!Array.isArray(fetchedEvents) || !fetchedEvents.length) {
break;
}
fetchedEvents = fetchedEvents.sort((a, b) => {
if (a.blockNumber === b.blockNumber) {
return a.logIndex - b.logIndex;
}
return a.blockNumber - b.blockNumber;
});
const [lastEvent] = fetchedEvents.slice(-1);
if (fetchedEvents.length < MAX_TOVARISH_EVENTS - 100) {
events.push(...fetchedEvents);
break;
}
fetchedEvents = fetchedEvents.filter(
(e) => e.blockNumber !== lastEvent.blockNumber,
);
fromBlock = Number(lastEvent.blockNumber);
events.push(...fetchedEvents);
}
return {
events,
lastSyncBlock,
};
} catch (err) {
console.log('Error from TovarishClient events endpoint');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
} }
if (recent) {
return {
events: fetchedEvents,
lastSyncBlock: currentBlock,
};
}
lastSyncBlock = currentBlock;
if (!Array.isArray(fetchedEvents) || !fetchedEvents.length) {
break;
}
fetchedEvents = fetchedEvents.sort((a, b) => {
if (a.blockNumber === b.blockNumber) {
return a.logIndex - b.logIndex;
}
return a.blockNumber - b.blockNumber;
});
const [lastEvent] = fetchedEvents.slice(-1);
if (fetchedEvents.length < MAX_TOVARISH_EVENTS - 100) {
events.push(...fetchedEvents);
break;
}
fetchedEvents = fetchedEvents.filter((e) => e.blockNumber !== lastEvent.blockNumber);
fromBlock = Number(lastEvent.blockNumber);
events.push(...fetchedEvents);
}
return {
events,
lastSyncBlock,
};
} catch (err) {
console.log('Error from TovarishClient events endpoint');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
} }
}
} }

View File

@@ -4,172 +4,197 @@ import type { BigNumberish } from 'ethers';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function () { (BigInt.prototype as any).toJSON = function () {
return this.toString(); return this.toString();
}; };
type bnInput = number | string | number[] | Uint8Array | Buffer | BN; type bnInput = number | string | number[] | Uint8Array | Buffer | BN;
export const isNode = export const isNode =
!( !(
process as typeof process & { process as typeof process & {
browser?: boolean; browser?: boolean;
} }
).browser && typeof globalThis.window === 'undefined'; ).browser && typeof globalThis.window === 'undefined';
export const crypto = isNode ? webcrypto : (globalThis.crypto as typeof webcrypto); export const crypto = isNode
? webcrypto
: (globalThis.crypto as typeof webcrypto);
export const chunk = <T>(arr: T[], size: number): T[][] => export const chunk = <T>(arr: T[], size: number): T[][] =>
[...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(size * i, size + size * i)); [...Array(Math.ceil(arr.length / size))].map((_, i) =>
arr.slice(size * i, size + size * i),
);
export function sleep(ms: number) { export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
export function validateUrl(url: string, protocols?: string[]) { export function validateUrl(url: string, protocols?: string[]) {
try { try {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
if (protocols && protocols.length) { if (protocols && protocols.length) {
return protocols.map((p) => p.toLowerCase()).includes(parsedUrl.protocol); return protocols
.map((p) => p.toLowerCase())
.includes(parsedUrl.protocol);
}
return true;
} catch {
return false;
} }
return true;
} catch {
return false;
}
} }
export function concatBytes(...arrays: Uint8Array[]): Uint8Array { export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
const totalSize = arrays.reduce((acc, e) => acc + e.length, 0); const totalSize = arrays.reduce((acc, e) => acc + e.length, 0);
const merged = new Uint8Array(totalSize); const merged = new Uint8Array(totalSize);
arrays.forEach((array, i, arrays) => { arrays.forEach((array, i, arrays) => {
const offset = arrays.slice(0, i).reduce((acc, e) => acc + e.length, 0); const offset = arrays.slice(0, i).reduce((acc, e) => acc + e.length, 0);
merged.set(array, offset); merged.set(array, offset);
}); });
return merged; return merged;
} }
export function bufferToBytes(b: Buffer) { export function bufferToBytes(b: Buffer) {
return new Uint8Array(b.buffer); return new Uint8Array(b.buffer);
} }
export function bytesToBase64(bytes: Uint8Array) { export function bytesToBase64(bytes: Uint8Array) {
return btoa(bytes.reduce((data, byte) => data + String.fromCharCode(byte), '')); return btoa(
bytes.reduce((data, byte) => data + String.fromCharCode(byte), ''),
);
} }
export function base64ToBytes(base64: string) { export function base64ToBytes(base64: string) {
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
} }
export function bytesToHex(bytes: Uint8Array) { export function bytesToHex(bytes: Uint8Array) {
return ( return (
'0x' + '0x' +
Array.from(bytes) Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0')) .map((b) => b.toString(16).padStart(2, '0'))
.join('') .join('')
); );
} }
export function hexToBytes(hexString: string) { export function hexToBytes(hexString: string) {
if (hexString.slice(0, 2) === '0x') { if (hexString.slice(0, 2) === '0x') {
hexString = hexString.slice(2); hexString = hexString.slice(2);
} }
if (hexString.length % 2 !== 0) { if (hexString.length % 2 !== 0) {
hexString = '0' + hexString; hexString = '0' + hexString;
} }
return Uint8Array.from((hexString.match(/.{1,2}/g) as string[]).map((byte) => parseInt(byte, 16))); return Uint8Array.from(
(hexString.match(/.{1,2}/g) as string[]).map((byte) =>
parseInt(byte, 16),
),
);
} }
// Convert BE encoded bytes (Buffer | Uint8Array) array to BigInt // Convert BE encoded bytes (Buffer | Uint8Array) array to BigInt
export function bytesToBN(bytes: Uint8Array) { export function bytesToBN(bytes: Uint8Array) {
return BigInt(bytesToHex(bytes)); return BigInt(bytesToHex(bytes));
} }
// Convert BigInt to BE encoded Uint8Array type // Convert BigInt to BE encoded Uint8Array type
export function bnToBytes(bigint: bigint | string) { export function bnToBytes(bigint: bigint | string) {
// Parse bigint to hex string // Parse bigint to hex string
let hexString: string = typeof bigint === 'bigint' ? bigint.toString(16) : bigint; let hexString: string =
// Remove hex string prefix if exists typeof bigint === 'bigint' ? bigint.toString(16) : bigint;
if (hexString.slice(0, 2) === '0x') { // Remove hex string prefix if exists
hexString = hexString.slice(2); if (hexString.slice(0, 2) === '0x') {
} hexString = hexString.slice(2);
// Hex string length should be a multiplier of two (To make correct bytes) }
if (hexString.length % 2 !== 0) { // Hex string length should be a multiplier of two (To make correct bytes)
hexString = '0' + hexString; if (hexString.length % 2 !== 0) {
} hexString = '0' + hexString;
return Uint8Array.from((hexString.match(/.{1,2}/g) as string[]).map((byte) => parseInt(byte, 16))); }
return Uint8Array.from(
(hexString.match(/.{1,2}/g) as string[]).map((byte) =>
parseInt(byte, 16),
),
);
} }
// Convert LE encoded bytes (Buffer | Uint8Array) array to BigInt // Convert LE encoded bytes (Buffer | Uint8Array) array to BigInt
export function leBuff2Int(bytes: Uint8Array) { export function leBuff2Int(bytes: Uint8Array) {
return new BN(bytes, 16, 'le'); return new BN(bytes, 16, 'le');
} }
// Convert BigInt to LE encoded Uint8Array type // Convert BigInt to LE encoded Uint8Array type
export function leInt2Buff(bigint: bnInput | bigint) { export function leInt2Buff(bigint: bnInput | bigint) {
return Uint8Array.from(new BN(bigint as bnInput).toArray('le', 31)); return Uint8Array.from(new BN(bigint as bnInput).toArray('le', 31));
} }
// Inherited from tornado-core and tornado-cli // Inherited from tornado-core and tornado-cli
export function toFixedHex(numberish: BigNumberish, length = 32) { export function toFixedHex(numberish: BigNumberish, length = 32) {
return ( return (
'0x' + '0x' +
BigInt(numberish) BigInt(numberish)
.toString(16) .toString(16)
.padStart(length * 2, '0') .padStart(length * 2, '0')
); );
} }
export function toFixedLength(string: string, length: number = 32) { export function toFixedLength(string: string, length: number = 32) {
string = string.replace('0x', ''); string = string.replace('0x', '');
return '0x' + string.padStart(length * 2, '0'); return '0x' + string.padStart(length * 2, '0');
} }
// Random BigInt in a range of bytes // Random BigInt in a range of bytes
export function rBigInt(nbytes: number = 31) { export function rBigInt(nbytes: number = 31) {
return bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes))); return bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes)));
} }
export function rHex(nbytes: number = 32) { export function rHex(nbytes: number = 32) {
return bytesToHex(crypto.getRandomValues(new Uint8Array(nbytes))); return bytesToHex(crypto.getRandomValues(new Uint8Array(nbytes)));
} }
// Used for JSON.stringify(value, bigIntReplacer, space) // Used for JSON.stringify(value, bigIntReplacer, space)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function bigIntReplacer(key: any, value: any) { export function bigIntReplacer(key: any, value: any) {
return typeof value === 'bigint' ? value.toString() : value; return typeof value === 'bigint' ? value.toString() : value;
} }
export function substring(str: string, length: number = 10) { export function substring(str: string, length: number = 10) {
if (str.length < length * 2) { if (str.length < length * 2) {
return str; return str;
} }
return `${str.substring(0, length)}...${str.substring(str.length - length)}`; return `${str.substring(0, length)}...${str.substring(str.length - length)}`;
} }
export async function digest(bytes: Uint8Array, algo: string = 'SHA-384') { export async function digest(bytes: Uint8Array, algo: string = 'SHA-384') {
return new Uint8Array(await crypto.subtle.digest(algo, bytes)); return new Uint8Array(await crypto.subtle.digest(algo, bytes));
} }
export function numberFormatter(num: string | number | bigint, digits: number = 3): string { export function numberFormatter(
const lookup = [ num: string | number | bigint,
{ value: 1, symbol: '' }, digits: number = 3,
{ value: 1e3, symbol: 'K' }, ): string {
{ value: 1e6, symbol: 'M' }, const lookup = [
{ value: 1e9, symbol: 'G' }, { value: 1, symbol: '' },
{ value: 1e12, symbol: 'T' }, { value: 1e3, symbol: 'K' },
{ value: 1e15, symbol: 'P' }, { value: 1e6, symbol: 'M' },
{ value: 1e18, symbol: 'E' }, { value: 1e9, symbol: 'G' },
]; { value: 1e12, symbol: 'T' },
const regexp = /\.0+$|(?<=\.[0-9]*[1-9])0+$/; { value: 1e15, symbol: 'P' },
const item = lookup { value: 1e18, symbol: 'E' },
.slice() ];
.reverse() const regexp = /\.0+$|(?<=\.[0-9]*[1-9])0+$/;
.find((item) => Number(num) >= item.value); const item = lookup
return item ? (Number(num) / item.value).toFixed(digits).replace(regexp, '').concat(item.symbol) : '0'; .slice()
.reverse()
.find((item) => Number(num) >= item.value);
return item
? (Number(num) / item.value)
.toFixed(digits)
.replace(regexp, '')
.concat(item.symbol)
: '0';
} }
export function isHex(value: string) { export function isHex(value: string) {
return /^0x[0-9a-fA-F]*$/.test(value); return /^0x[0-9a-fA-F]*$/.test(value);
} }

View File

@@ -6,81 +6,86 @@ import type { Element } from '@tornado/fixed-merkle-tree';
import { toFixedHex } from './utils'; import { toFixedHex } from './utils';
export interface snarkInputs { export interface snarkInputs {
// Public snark inputs // Public snark inputs
root: Element; root: Element;
nullifierHex: string; nullifierHex: string;
recipient: string; recipient: string;
relayer: string; relayer: string;
fee: bigint; fee: bigint;
refund: bigint; refund: bigint;
// Private snark inputs // Private snark inputs
nullifier: bigint; nullifier: bigint;
secret: bigint; secret: bigint;
pathElements: Element[]; pathElements: Element[];
pathIndices: Element[]; pathIndices: Element[];
} }
export type snarkArgs = [ export type snarkArgs = [
_root: string, _root: string,
_nullifierHash: string, _nullifierHash: string,
_recipient: string, _recipient: string,
_relayer: string, _relayer: string,
_fee: string, _fee: string,
_refund: string, _refund: string,
]; ];
export interface snarkProofs { export interface snarkProofs {
proof: string; proof: string;
args: snarkArgs; args: snarkArgs;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let groth16: any; let groth16: any;
export async function initGroth16() { export async function initGroth16() {
if (!groth16) { if (!groth16) {
groth16 = await websnarkGroth({ wasmInitialMemory: 2000 }); groth16 = await websnarkGroth({ wasmInitialMemory: 2000 });
} }
} }
export async function calculateSnarkProof( export async function calculateSnarkProof(
input: snarkInputs, input: snarkInputs,
circuit: object, circuit: object,
provingKey: ArrayBuffer, provingKey: ArrayBuffer,
): Promise<snarkProofs> { ): Promise<snarkProofs> {
if (!groth16) { if (!groth16) {
await initGroth16(); await initGroth16();
} }
const snarkInput = { const snarkInput = {
root: input.root, root: input.root,
nullifierHash: BigInt(input.nullifierHex).toString(), nullifierHash: BigInt(input.nullifierHex).toString(),
recipient: BigInt(input.recipient as string), recipient: BigInt(input.recipient as string),
relayer: BigInt(input.relayer as string), relayer: BigInt(input.relayer as string),
fee: input.fee, fee: input.fee,
refund: input.refund, refund: input.refund,
nullifier: input.nullifier, nullifier: input.nullifier,
secret: input.secret, secret: input.secret,
pathElements: input.pathElements, pathElements: input.pathElements,
pathIndices: input.pathIndices, pathIndices: input.pathIndices,
}; };
console.log('Start generating SNARK proof', snarkInput); console.log('Start generating SNARK proof', snarkInput);
console.time('SNARK proof time'); console.time('SNARK proof time');
const proofData = await websnarkUtils.genWitnessAndProve(await groth16, snarkInput, circuit, provingKey); const proofData = await websnarkUtils.genWitnessAndProve(
const proof = websnarkUtils.toSolidityInput(proofData).proof; await groth16,
console.timeEnd('SNARK proof time'); snarkInput,
circuit,
provingKey,
);
const proof = websnarkUtils.toSolidityInput(proofData).proof;
console.timeEnd('SNARK proof time');
const args = [ const args = [
toFixedHex(input.root, 32), toFixedHex(input.root, 32),
toFixedHex(input.nullifierHex, 32), toFixedHex(input.nullifierHex, 32),
input.recipient, input.recipient,
input.relayer, input.relayer,
toFixedHex(input.fee, 32), toFixedHex(input.fee, 32),
toFixedHex(input.refund, 32), toFixedHex(input.refund, 32),
] as snarkArgs; ] as snarkArgs;
return { proof, args }; return { proof, args };
} }

View File

@@ -3,66 +3,68 @@ import { fetchData } from './providers';
import { bytesToBase64, digest } from './utils'; import { bytesToBase64, digest } from './utils';
export function zipAsync(file: AsyncZippable): Promise<Uint8Array> { export function zipAsync(file: AsyncZippable): Promise<Uint8Array> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
zip(file, { mtime: new Date('1/1/1980') }, (err, data) => { zip(file, { mtime: new Date('1/1/1980') }, (err, data) => {
if (err) { if (err) {
rej(err); rej(err);
return; return;
} }
res(data); res(data);
});
}); });
});
} }
export function unzipAsync(data: Uint8Array): Promise<Unzipped> { export function unzipAsync(data: Uint8Array): Promise<Unzipped> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
unzip(data, {}, (err, data) => { unzip(data, {}, (err, data) => {
if (err) { if (err) {
rej(err); rej(err);
return; return;
} }
res(data); res(data);
});
}); });
});
} }
export async function downloadZip<T>({ export async function downloadZip<T>({
staticUrl = '', staticUrl = '',
zipName, zipName,
zipDigest, zipDigest,
parseJson = true, parseJson = true,
}: { }: {
staticUrl?: string; staticUrl?: string;
zipName: string; zipName: string;
zipDigest?: string; zipDigest?: string;
parseJson?: boolean; parseJson?: boolean;
}): Promise<T> { }): Promise<T> {
const url = `${staticUrl}/${zipName}.zip`; const url = `${staticUrl}/${zipName}.zip`;
const resp = (await fetchData(url, { const resp = (await fetchData(url, {
method: 'GET', method: 'GET',
returnResponse: true, returnResponse: true,
})) as Response; })) as Response;
const data = new Uint8Array(await resp.arrayBuffer()); const data = new Uint8Array(await resp.arrayBuffer());
// If the zip has digest value, compare it // If the zip has digest value, compare it
if (zipDigest) { if (zipDigest) {
const hash = 'sha384-' + bytesToBase64(await digest(data)); const hash = 'sha384-' + bytesToBase64(await digest(data));
if (zipDigest !== hash) { if (zipDigest !== hash) {
const errMsg = `Invalid digest hash for file ${url}, wants ${zipDigest} has ${hash}`; const errMsg = `Invalid digest hash for file ${url}, wants ${zipDigest} has ${hash}`;
throw new Error(errMsg); throw new Error(errMsg);
}
} }
}
const { [zipName]: content } = await unzipAsync(data); const { [zipName]: content } = await unzipAsync(data);
console.log(`Downloaded ${url}${zipDigest ? ` ( Digest: ${zipDigest} )` : ''}`); console.log(
`Downloaded ${url}${zipDigest ? ` ( Digest: ${zipDigest} )` : ''}`,
);
if (parseJson) { if (parseJson) {
return JSON.parse(new TextDecoder().decode(content)) as T; return JSON.parse(new TextDecoder().decode(content)) as T;
} }
return content as T; return content as T;
} }

View File

@@ -12,47 +12,65 @@ const { getSigners } = ethers;
const NOTES_COUNT = 100; const NOTES_COUNT = 100;
describe('./src/deposit.ts', function () { describe('./src/deposit.ts', function () {
const instanceFixture = async () => { const instanceFixture = async () => {
const [owner] = await getSigners(); const [owner] = await getSigners();
const Hasher = (await (await deployHasher(owner)).wait())?.contractAddress as string; const Hasher = (await (await deployHasher(owner)).wait())
?.contractAddress as string;
const Verifier = await new Verifier__factory(owner).deploy(); const Verifier = await new Verifier__factory(owner).deploy();
const Instance = await new ETHTornado__factory(owner).deploy(Verifier.target, Hasher, 1n, 20); const Instance = await new ETHTornado__factory(owner).deploy(
Verifier.target,
Hasher,
1n,
20,
);
return { Instance }; return { Instance };
}; };
it('Deposit New Note', async function () { it('Deposit New Note', async function () {
const { Instance } = await loadFixture(instanceFixture); const { Instance } = await loadFixture(instanceFixture);
const [owner] = await getSigners(); const [owner] = await getSigners();
const netId = Number((await owner.provider.getNetwork()).chainId); const netId = Number((await owner.provider.getNetwork()).chainId);
const deposit = await Deposit.createNote({ currency: 'eth', amount: formatEther(1), netId }); const deposit = await Deposit.createNote({
currency: 'eth',
amount: formatEther(1),
netId,
});
const resp = await Instance.deposit(deposit.commitmentHex, { value: 1n }); const resp = await Instance.deposit(deposit.commitmentHex, {
value: 1n,
});
await expect(resp).to.emit(Instance, 'Deposit').withArgs(deposit.commitmentHex, 0, anyValue); await expect(resp)
.to.emit(Instance, 'Deposit')
.withArgs(deposit.commitmentHex, 0, anyValue);
expect(await Instance.commitments(deposit.commitmentHex)).to.be.true; expect(await Instance.commitments(deposit.commitmentHex)).to.be.true;
}); });
xit(`Creating ${NOTES_COUNT} random notes`, async function () { xit(`Creating ${NOTES_COUNT} random notes`, async function () {
const notes = (await Promise.all( const notes = (await Promise.all(
// eslint-disable-next-line prefer-spread // eslint-disable-next-line prefer-spread
Array.apply(null, Array(NOTES_COUNT)).map(() => Array.apply(null, Array(NOTES_COUNT)).map(() =>
Deposit.createNote({ currency: 'eth', amount: '0.1', netId: 31337 }), Deposit.createNote({
), currency: 'eth',
)) as Deposit[]; amount: '0.1',
netId: 31337,
notes.forEach(({ noteHex, commitmentHex, nullifierHex }) => { }),
// ((secret.length: 31) + (nullifier.length: 31)) * 2 + (prefix: 2) = 126 ),
expect(noteHex.length === 126).to.be.true; )) as Deposit[];
expect(commitmentHex.length === 66).to.be.true;
expect(nullifierHex.length === 66).to.be.true; notes.forEach(({ noteHex, commitmentHex, nullifierHex }) => {
// ((secret.length: 31) + (nullifier.length: 31)) * 2 + (prefix: 2) = 126
expect(noteHex.length === 126).to.be.true;
expect(commitmentHex.length === 66).to.be.true;
expect(nullifierHex.length === 66).to.be.true;
});
}); });
});
}); });

View File

@@ -2,13 +2,13 @@ import { describe } from 'mocha';
import { ethers } from 'hardhat'; import { ethers } from 'hardhat';
describe('Tornado Core', function () { describe('Tornado Core', function () {
it('Get Provider', async function () { it('Get Provider', async function () {
const [owner] = await ethers.getSigners(); const [owner] = await ethers.getSigners();
console.log(owner); console.log(owner);
const { provider } = owner; const { provider } = owner;
console.log(await provider.getBlock('latest')); console.log(await provider.getBlock('latest'));
}); });
}); });

View File

@@ -3,171 +3,171 @@ const path = require('path');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const esbuildLoader = { const esbuildLoader = {
test: /\.ts?$/, test: /\.ts?$/,
loader: 'esbuild-loader', loader: 'esbuild-loader',
options: { options: {
loader: 'ts', loader: 'ts',
target: 'es2022', target: 'es2022',
} }
} }
const commonAlias = { const commonAlias = {
fs: false, fs: false,
'path': false, 'path': false,
'url': false, 'url': false,
'worker_threads': false, 'worker_threads': false,
'fflate': 'fflate/browser', 'fflate': 'fflate/browser',
'http-proxy-agent': false, 'http-proxy-agent': false,
'https-proxy-agent': false, 'https-proxy-agent': false,
'socks-proxy-agent': false, 'socks-proxy-agent': false,
} }
module.exports = [ module.exports = [
{ {
mode: 'production', mode: 'production',
module: { module: {
rules: [esbuildLoader] rules: [esbuildLoader]
},
entry: './src/index.ts',
output: {
filename: 'tornado.umd.js',
path: path.resolve(__dirname, './dist'),
library: 'Tornado',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
}, },
entry: './src/index.ts', {
output: { mode: 'production',
filename: 'tornado.umd.js', module: {
path: path.resolve(__dirname, './dist'), rules: [esbuildLoader]
library: 'Tornado', },
libraryTarget: 'umd' entry: './src/index.ts',
output: {
filename: 'tornado.umd.min.js',
path: path.resolve(__dirname, './dist'),
library: 'Tornado',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
}, },
plugins: [ {
new NodePolyfillPlugin(), mode: 'production',
], module: {
resolve: { rules: [esbuildLoader]
extensions: ['.tsx', '.ts', '.js'], },
alias: { entry: './src/merkleTreeWorker.ts',
...commonAlias, output: {
} filename: 'merkleTreeWorker.umd.js',
path: path.resolve(__dirname, './dist'),
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
new BannerPlugin({
banner: 'globalThis.process = { browser: true, env: {}, };\n',
raw: true,
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
}, },
optimization: { {
minimize: false, mode: 'production',
} module: {
}, rules: [esbuildLoader]
{ },
mode: 'production', entry: './src/merkleTreeWorker.ts',
module: { output: {
rules: [esbuildLoader] filename: 'merkleTreeWorker.umd.min.js',
path: path.resolve(__dirname, './dist'),
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
new BannerPlugin({
banner: 'globalThis.process = { browser: true, env: {}, };',
raw: true,
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
}, },
entry: './src/index.ts', {
output: { mode: 'production',
filename: 'tornado.umd.min.js', module: {
path: path.resolve(__dirname, './dist'), rules: [esbuildLoader]
library: 'Tornado', },
libraryTarget: 'umd' entry: './src/contracts.ts',
output: {
filename: 'tornadoContracts.umd.js',
path: path.resolve(__dirname, './dist'),
library: 'TornadoContracts',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
}, },
plugins: [ {
new NodePolyfillPlugin(), mode: 'production',
], module: {
resolve: { rules: [esbuildLoader]
extensions: ['.tsx', '.ts', '.js'], },
alias: { entry: './src/contracts.ts',
...commonAlias, output: {
} filename: 'tornadoContracts.umd.min.js',
path: path.resolve(__dirname, './dist'),
library: 'TornadoContracts',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
}, },
},
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/merkleTreeWorker.ts',
output: {
filename: 'merkleTreeWorker.umd.js',
path: path.resolve(__dirname, './dist'),
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
new BannerPlugin({
banner: 'globalThis.process = { browser: true, env: {}, };\n',
raw: true,
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
},
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/merkleTreeWorker.ts',
output: {
filename: 'merkleTreeWorker.umd.min.js',
path: path.resolve(__dirname, './dist'),
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
new BannerPlugin({
banner: 'globalThis.process = { browser: true, env: {}, };',
raw: true,
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
},
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/contracts.ts',
output: {
filename: 'tornadoContracts.umd.js',
path: path.resolve(__dirname, './dist'),
library: 'TornadoContracts',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
optimization: {
minimize: false,
}
},
{
mode: 'production',
module: {
rules: [esbuildLoader]
},
entry: './src/contracts.ts',
output: {
filename: 'tornadoContracts.umd.min.js',
path: path.resolve(__dirname, './dist'),
library: 'TornadoContracts',
libraryTarget: 'umd'
},
plugins: [
new NodePolyfillPlugin(),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
...commonAlias,
}
},
},
]; ];