Compare commits
8 Commits
6006120e60
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3491bffeef | |||
| c6fea739bc | |||
| db1d4f9b6f | |||
| 55fc28dacf | |||
| 21fc07617c | |||
|
|
e676b48b59 | ||
|
|
19124126f4 | ||
|
|
2f28a7e627 |
3
.env.bat.example
Normal file
3
.env.bat.example
Normal file
@@ -0,0 +1,3 @@
|
||||
set TARGET_REGISTRY_USER=
|
||||
set TARGET_REGISTRY_DOMAIN=
|
||||
set GITEA_AUTH_TOKEN=
|
||||
@@ -1 +1,4 @@
|
||||
#!/bin/bash
|
||||
export TARGET_REGISTRY_USER=
|
||||
export TARGET_REGISTRY_DOMAIN=
|
||||
export GITEA_AUTH_TOKEN=
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@
|
||||
# Environment
|
||||
|
||||
.env
|
||||
.env.bat
|
||||
|
||||
# node modules
|
||||
|
||||
|
||||
21
.yarnrc.yml
21
.yarnrc.yml
@@ -1,26 +1,19 @@
|
||||
# So basically, we want to selectively download packages from the Gitea repository but still allow the
|
||||
# So basically, we want to selectively download packages from the Gitea repository but still allow the
|
||||
# main repositories, so we are going to have to scope our packages properly.
|
||||
npmScopes:
|
||||
tornado:
|
||||
# The @tornado scope can just be set to the right registry by editing the T-Hax here.
|
||||
# I could even make it an env var but it seems too much, that is for secrets.
|
||||
npmPublishRegistry: "https://development.tornadocash.community/api/packages/T-Hax/npm/"
|
||||
npmRegistryServer: "https://development.tornadocash.community/api/packages/T-Hax/npm/"
|
||||
# The below can be EXPORTED via some env file which actually exports the variables though
|
||||
# Check the env.example, you will notice it's not a regular env
|
||||
# So split your env files up, since this is only for manual actions
|
||||
npmAuthToken: ${GITEA_AUTH_TOKEN}
|
||||
# Since there can be multiple repositories we select them via the .env we use `source .env` on Linux or `cmd.exe /c .env.bat` on Windows.
|
||||
# Check .env.example or .env.example.bat
|
||||
npmRegistryServer: https://${TARGET_REGISTRY_DOMAIN}/api/packages/${TARGET_REGISTRY_USER}/npm/
|
||||
|
||||
npmPublishRegistry: https://${TARGET_REGISTRY_DOMAIN}/api/packages/${TARGET_REGISTRY_USER}/npm/
|
||||
npmAuthToken: ${GITEA_AUTH_TOKEN}
|
||||
|
||||
# If the following isn't set you won't have a node_modules folder
|
||||
# You might be used to a node_modules folder instead of a pnp or other folder type
|
||||
# If you want to use the new linkers uncomment or change the following
|
||||
nodeLinker: "node-modules"
|
||||
|
||||
# So the tornadocash org, the person who maintains it, might setup something (in future) like,
|
||||
# tornadocash:
|
||||
# npmPublishRegistry: "https://development.tornadocash.community/api/packages/tornadocash/npm/"
|
||||
# npmRegistryServer: "https://development.tornadocash.community/api/packages/tornadocash/npm/"
|
||||
|
||||
# We disable telemetry for obvious reasons. enableTelemetry: false
|
||||
enableTelemetry: false
|
||||
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
"circom",
|
||||
"zksnark"
|
||||
],
|
||||
"homepage": "https://git.tornado.ws/tornado-packages/circomlib",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://development.tornadocash.community/tornadocash/circomlib"
|
||||
"url": "https://git.tornado.ws/tornado-packages/archive-monorepo"
|
||||
},
|
||||
"author": "0Kims",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
9
@tornado/fixed-merkle-tree/.editorconfig
Normal file
9
@tornado/fixed-merkle-tree/.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
57
@tornado/fixed-merkle-tree/.eslintrc
Normal file
57
@tornado/fixed-merkle-tree/.eslintrc
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"test/*.spec.ts",
|
||||
"lib"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"require-await": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
}
|
||||
}
|
||||
78
@tornado/fixed-merkle-tree/.gitignore
vendored
78
@tornado/fixed-merkle-tree/.gitignore
vendored
@@ -1,74 +1,6 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
node_modules
|
||||
build
|
||||
yarn-error.log
|
||||
.idea
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
tmp
|
||||
|
||||
.DS_Store
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.run
|
||||
|
||||
1
@tornado/fixed-merkle-tree/.nvmrc
Normal file
1
@tornado/fixed-merkle-tree/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
14.17
|
||||
7
@tornado/fixed-merkle-tree/.prettierrc
Normal file
7
@tornado/fixed-merkle-tree/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"printWidth": 110,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
# @T-Hax/fixed-merkle-tree
|
||||
|
||||
This repository serves to configure the equivalent Tornado Cash repository for npm publishing. Below the rest of the description.
|
||||
|
||||
# Merkle Tree [](https://github.com/tornadocash/fixed-merkle-tree/actions) [](https://www.npmjs.com/package/fixed-merkle-tree)
|
||||
|
||||
This is a fixed depth merkle tree implementation with sequential inserts
|
||||
@@ -9,27 +5,68 @@ This is a fixed depth merkle tree implementation with sequential inserts
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
const MerkleTree = require('MerkleTree')
|
||||
import { MerkleTree, PartialMerkleTree } from 'fixed-merkle-tree'
|
||||
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.insert(6)
|
||||
tree.update(3, 42)
|
||||
const path = tree.path(tree.indexOf(2))
|
||||
const path = tree.proof(3)
|
||||
console.log(path)
|
||||
|
||||
// output:
|
||||
// output
|
||||
{
|
||||
pathIndex: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
pathElements: [
|
||||
'42',
|
||||
'19814528709687996974327303300007262407299502847885145507292406548098437687919',
|
||||
'11545490348087423460235196042660837039811055736960842865648632633825765931887',
|
||||
'14506027710748750947258687001455876266559341618222612722926156490737302846427',
|
||||
'4766583705360062980279572762279781527342845808161105063909171241304075622345',
|
||||
'16640205414190175414380077665118269450294358858897019640557533278896634808665',
|
||||
'13024477302430254842915163302704885770955784224100349847438808884122720088412',
|
||||
'11345696205391376769769683860277269518617256738724086786512014734609753488820',
|
||||
'17235543131546745471991808272245772046758360534180976603221801364506032471936',
|
||||
'155962837046691114236524362966874066300454611955781275944230309195800494087'
|
||||
]
|
||||
42,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'2008015086710634950773855228781840564224',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456'
|
||||
],
|
||||
pathIndices: [
|
||||
0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathPositions: [
|
||||
3, 0, 1, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathRoot: '3917789723822252567979048877718291611648'
|
||||
}
|
||||
|
||||
const treeEdge = tree.getTreeEdge(2)
|
||||
const partialTree = new PartialMerkleTree(10, treeEdge, tree.elements.slice(treeEdge.edgeIndex))
|
||||
console.log(partialTree.elements)
|
||||
// [<2 empty items >, 3, 42, 5, 6]
|
||||
|
||||
const proofPath = partialTree.proof(3)
|
||||
console.log(proofPath)
|
||||
// output
|
||||
{
|
||||
pathElements: [
|
||||
42,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'2008015086710634950773855228781840564224',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456'
|
||||
],
|
||||
pathIndices: [
|
||||
0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathPositions: [
|
||||
3, 0, 1, 0, 0,
|
||||
0, 0, 0, 0, 0
|
||||
],
|
||||
pathRoot: '3917789723822252567979048877718291611648'
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
112
@tornado/fixed-merkle-tree/index.d.ts
vendored
112
@tornado/fixed-merkle-tree/index.d.ts
vendored
@@ -1,112 +0,0 @@
|
||||
declare module "src/mimc" {
|
||||
function _exports(left: any, right: any): any;
|
||||
export = _exports;
|
||||
}
|
||||
declare module "fixed-merkle-tree/src/merkleTree" {
|
||||
export = MerkleTree;
|
||||
/**
|
||||
* @callback hashFunction
|
||||
* @param left Left leaf
|
||||
* @param right Right leaf
|
||||
*/
|
||||
/**
|
||||
* Merkle tree
|
||||
*/
|
||||
export class MerkleTree {
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*
|
||||
* @param data
|
||||
* @param hashFunction
|
||||
* @returns {MerkleTree}
|
||||
*/
|
||||
static deserialize(data: any, hashFunction: any): MerkleTree;
|
||||
/**
|
||||
* Constructor
|
||||
* @param {number} levels Number of levels in the tree
|
||||
* @param {Array} [elements] Initial elements
|
||||
* @param {Object} options
|
||||
* @param {hashFunction} [options.hashFunction] Function used to hash 2 leaves
|
||||
* @param [options.zeroElement] Value for non-existent leaves
|
||||
*/
|
||||
constructor(levels: number, elements?: any[], { hashFunction, zeroElement }?: {
|
||||
hashFunction?: hashFunction;
|
||||
zeroElement?: any;
|
||||
});
|
||||
levels: number;
|
||||
capacity: number;
|
||||
_hash: (left: any, right: any) => any;
|
||||
zeroElement: any;
|
||||
_zeros: any[];
|
||||
_layers: any[][];
|
||||
_rebuild(): void;
|
||||
/**
|
||||
* Get tree root
|
||||
* @returns {*}
|
||||
*/
|
||||
root(): any;
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element: any): void;
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: any[]): void;
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: any): void;
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index: number): {
|
||||
pathElements: any[];
|
||||
pathIndex: number[];
|
||||
};
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @returns {number} Index if element is found, otherwise -1
|
||||
*/
|
||||
indexOf(element: any, comparator: any): number;
|
||||
/**
|
||||
* Returns a copy of non-zero tree elements
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
elements(): any[];
|
||||
/**
|
||||
* Returns a copy of n-th zero elements array
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
zeros(): any[];
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize(): {
|
||||
levels: number;
|
||||
_zeros: any[];
|
||||
_layers: any[][];
|
||||
};
|
||||
}
|
||||
namespace MerkleTree {
|
||||
export { hashFunction };
|
||||
}
|
||||
type hashFunction = (left: any, right: any) => any;
|
||||
}
|
||||
declare module "fixed-merkle-tree" {
|
||||
const _exports: typeof import("fixed-merkle-tree/src/merkleTree");
|
||||
export = _exports;
|
||||
}
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/mimc.js","src/merkleTree.js","index.js"],"names":[],"mappings":";IAEiB,8CAA+E;;;;;ICKhG;;;;OAIG;IACH;;OAEG;IACH;QAkLE;;;;;;;;WAQG;QACH,kDAFa,UAAU,CAQtB;QAhMD;;;;;;;WAOG;QACH,oBANW,MAAM;YAGiB,YAAY,GAAnC,YAAY;YACJ,WAAW;WAkB7B;QAfC,eAAoB;QACpB,iBAA2B;QAI3B,sCAAwC;QACxC,iBAA8B;QAC9B,cAAgB;QAKhB,iBAAiB;QAKnB,iBAYC;QAED;;;WAGG;QACH,YAEC;QAED;;;WAGG;QACH,2BAKC;QAED;;;WAGG;QACH,kCAyBC;QAED;;;;WAIG;QACH,cAHW,MAAM,sBAiBhB;QAED;;;;WAIG;QACH,YAHW,MAAM;0BACW,KAAQ;uBAAa,MAAM,EAAE;UAkBxD;QAED;;;;;WAKG;QACH,wCAFa,MAAM,CAQlB;QAED;;;WAGG;QACH,YAFa,KAAQ,CAIpB;QAED;;;WAGG;QACH,SAFa,KAAQ,CAIpB;QAED;;;;WAIG;QACH;;;;UAMC;KAkBF"}
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("./src/merkleTree")
|
||||
43
@tornado/fixed-merkle-tree/lib/BaseTree.d.ts
vendored
Normal file
43
@tornado/fixed-merkle-tree/lib/BaseTree.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Element, HashFunction, ProofPath } from './';
|
||||
export declare class BaseTree {
|
||||
levels: number;
|
||||
protected _hashFn: HashFunction<Element>;
|
||||
protected zeroElement: Element;
|
||||
protected _zeros: Element[];
|
||||
protected _layers: Array<Element[]>;
|
||||
get capacity(): number;
|
||||
get layers(): Array<Element[]>;
|
||||
get zeros(): Element[];
|
||||
get elements(): Element[];
|
||||
get root(): Element;
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param elements elements of tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @param fromIndex The index to start the search at. If the index is greater than or equal to the array's length, -1 is returned
|
||||
* @returns {number} Index if element is found, otherwise -1
|
||||
*/
|
||||
static indexOf(elements: Element[], element: Element, fromIndex?: number, comparator?: <T>(arg0: T, arg1: T) => boolean): number;
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element: Element): void;
|
||||
bulkInsert(elements: Element[]): void;
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element): void;
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index: number): ProofPath;
|
||||
protected _buildZeros(): void;
|
||||
protected _processNodes(nodes: Element[], layerIndex: number): any[];
|
||||
protected _processUpdate(index: number): void;
|
||||
}
|
||||
154
@tornado/fixed-merkle-tree/lib/BaseTree.js
Normal file
154
@tornado/fixed-merkle-tree/lib/BaseTree.js
Normal file
@@ -0,0 +1,154 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BaseTree = void 0;
|
||||
class BaseTree {
|
||||
get capacity() {
|
||||
return 2 ** this.levels;
|
||||
}
|
||||
get layers() {
|
||||
return this._layers.slice();
|
||||
}
|
||||
get zeros() {
|
||||
return this._zeros.slice();
|
||||
}
|
||||
get elements() {
|
||||
return this._layers[0].slice();
|
||||
}
|
||||
get root() {
|
||||
var _a;
|
||||
return (_a = this._layers[this.levels][0]) !== null && _a !== void 0 ? _a : this._zeros[this.levels];
|
||||
}
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param elements elements of tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @param fromIndex The index to start the search at. If the index is greater than or equal to the array's length, -1 is returned
|
||||
* @returns {number} Index if element is found, otherwise -1
|
||||
*/
|
||||
static indexOf(elements, element, fromIndex, comparator) {
|
||||
if (comparator) {
|
||||
return elements.findIndex((el) => comparator(element, el));
|
||||
}
|
||||
else {
|
||||
return elements.indexOf(element, fromIndex);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element) {
|
||||
if (this._layers[0].length >= this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
this.update(this._layers[0].length, element);
|
||||
}
|
||||
/*
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements) {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i]);
|
||||
let level = 0;
|
||||
let index = this._layers[0].length - 1;
|
||||
while (index % 2 === 1) {
|
||||
level++;
|
||||
index >>= 1;
|
||||
const left = this._layers[level - 1][index * 2];
|
||||
const right = this._layers[level - 1][index * 2 + 1];
|
||||
this._layers[level][index] = this._hashFn(left, right);
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1]);
|
||||
}
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index, element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index);
|
||||
}
|
||||
this._layers[0][index] = element;
|
||||
this._processUpdate(index);
|
||||
}
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index) {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index);
|
||||
}
|
||||
let elIndex = +index;
|
||||
const pathElements = [];
|
||||
const pathIndices = [];
|
||||
const pathPositions = [];
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2;
|
||||
const leafIndex = elIndex ^ 1;
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex];
|
||||
pathPositions[level] = leafIndex;
|
||||
}
|
||||
else {
|
||||
pathElements[level] = this._zeros[level];
|
||||
pathPositions[level] = 0;
|
||||
}
|
||||
elIndex >>= 1;
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
};
|
||||
}
|
||||
_buildZeros() {
|
||||
this._zeros = [this.zeroElement];
|
||||
for (let i = 1; i <= this.levels; i++) {
|
||||
this._zeros[i] = this._hashFn(this._zeros[i - 1], this._zeros[i - 1]);
|
||||
}
|
||||
}
|
||||
_processNodes(nodes, layerIndex) {
|
||||
const length = nodes.length;
|
||||
let currentLength = Math.ceil(length / 2);
|
||||
const currentLayer = new Array(currentLength);
|
||||
currentLength--;
|
||||
const starFrom = length - ((length % 2) ^ 1);
|
||||
let j = 0;
|
||||
for (let i = starFrom; i >= 0; i -= 2) {
|
||||
if (nodes[i - 1] === undefined)
|
||||
break;
|
||||
const left = nodes[i - 1];
|
||||
const right = (i === starFrom && length % 2 === 1) ? this._zeros[layerIndex - 1] : nodes[i];
|
||||
currentLayer[currentLength - j] = this._hashFn(left, right);
|
||||
j++;
|
||||
}
|
||||
return currentLayer;
|
||||
}
|
||||
_processUpdate(index) {
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
index >>= 1;
|
||||
const left = this._layers[level - 1][index * 2];
|
||||
const right = index * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][index * 2 + 1]
|
||||
: this._zeros[level - 1];
|
||||
this._layers[level][index] = this._hashFn(left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.BaseTree = BaseTree;
|
||||
32
@tornado/fixed-merkle-tree/lib/FixedMerkleTree.d.ts
vendored
Normal file
32
@tornado/fixed-merkle-tree/lib/FixedMerkleTree.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './';
|
||||
import { BaseTree } from './BaseTree';
|
||||
export default class MerkleTree extends BaseTree {
|
||||
constructor(levels: number, elements?: Element[], { hashFunction, zeroElement, }?: MerkleTreeOptions);
|
||||
private _buildHashes;
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: Element[]): void;
|
||||
indexOf(element: Element, comparator?: <T>(arg0: T, arg1: T) => boolean): number;
|
||||
proof(element: Element): ProofPath;
|
||||
getTreeEdge(edgeIndex: number): TreeEdge;
|
||||
/**
|
||||
* 🪓
|
||||
* @param count
|
||||
*/
|
||||
getTreeSlices(count?: number): TreeSlice[];
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize(): SerializedTreeState;
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*/
|
||||
static deserialize(data: SerializedTreeState, hashFunction?: HashFunction<Element>): MerkleTree;
|
||||
toString(): string;
|
||||
}
|
||||
114
@tornado/fixed-merkle-tree/lib/FixedMerkleTree.js
Normal file
114
@tornado/fixed-merkle-tree/lib/FixedMerkleTree.js
Normal file
@@ -0,0 +1,114 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const simpleHash_1 = __importDefault(require("./simpleHash"));
|
||||
const BaseTree_1 = require("./BaseTree");
|
||||
class MerkleTree extends BaseTree_1.BaseTree {
|
||||
constructor(levels, elements = [], { hashFunction = simpleHash_1.default, zeroElement = 0, } = {}) {
|
||||
super();
|
||||
this.levels = levels;
|
||||
if (elements.length > this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
this._hashFn = hashFunction;
|
||||
this.zeroElement = zeroElement;
|
||||
this._layers = [];
|
||||
const leaves = elements.slice();
|
||||
this._layers = [leaves];
|
||||
this._buildZeros();
|
||||
this._buildHashes();
|
||||
}
|
||||
_buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1];
|
||||
this._layers[layerIndex] = this._processNodes(nodes, layerIndex);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements) {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full');
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i]);
|
||||
let level = 0;
|
||||
let index = this._layers[0].length - 1;
|
||||
while (index % 2 === 1) {
|
||||
level++;
|
||||
index >>= 1;
|
||||
this._layers[level][index] = this._hashFn(this._layers[level - 1][index * 2], this._layers[level - 1][index * 2 + 1]);
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1]);
|
||||
}
|
||||
indexOf(element, comparator) {
|
||||
return BaseTree_1.BaseTree.indexOf(this._layers[0], element, 0, comparator);
|
||||
}
|
||||
proof(element) {
|
||||
const index = this.indexOf(element);
|
||||
return this.path(index);
|
||||
}
|
||||
getTreeEdge(edgeIndex) {
|
||||
const edgeElement = this._layers[0][edgeIndex];
|
||||
if (edgeElement === undefined) {
|
||||
throw new Error('Element not found');
|
||||
}
|
||||
const edgePath = this.path(edgeIndex);
|
||||
return { edgePath, edgeElement, edgeIndex, edgeElementsCount: this._layers[0].length };
|
||||
}
|
||||
/**
|
||||
* 🪓
|
||||
* @param count
|
||||
*/
|
||||
getTreeSlices(count = 4) {
|
||||
const length = this._layers[0].length;
|
||||
let size = Math.ceil(length / count);
|
||||
if (size % 2)
|
||||
size++;
|
||||
const slices = [];
|
||||
for (let i = 0; i < length; i += size) {
|
||||
const edgeLeft = i;
|
||||
const edgeRight = i + size;
|
||||
slices.push({ edge: this.getTreeEdge(edgeLeft), elements: this.elements.slice(edgeLeft, edgeRight) });
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize() {
|
||||
return {
|
||||
levels: this.levels,
|
||||
_zeros: this._zeros,
|
||||
_layers: this._layers,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*/
|
||||
static deserialize(data, hashFunction) {
|
||||
const instance = Object.assign(Object.create(this.prototype), data);
|
||||
instance._hashFn = hashFunction || simpleHash_1.default;
|
||||
instance.zeroElement = instance._zeros[0];
|
||||
return instance;
|
||||
}
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize());
|
||||
}
|
||||
}
|
||||
exports.default = MerkleTree;
|
||||
35
@tornado/fixed-merkle-tree/lib/PartialMerkleTree.d.ts
vendored
Normal file
35
@tornado/fixed-merkle-tree/lib/PartialMerkleTree.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedPartialTreeState, TreeEdge } from './';
|
||||
import { BaseTree } from './BaseTree';
|
||||
export declare class PartialMerkleTree extends BaseTree {
|
||||
private _leaves;
|
||||
private _leavesAfterEdge;
|
||||
private _edgeLeaf;
|
||||
private _initialRoot;
|
||||
private _edgeLeafProof;
|
||||
private _proofMap;
|
||||
constructor(levels: number, { edgePath, edgeElement, edgeIndex, edgeElementsCount, }: TreeEdge, leaves: Element[], { hashFunction, zeroElement }?: MerkleTreeOptions);
|
||||
get edgeIndex(): number;
|
||||
get edgeElement(): Element;
|
||||
get edgeLeafProof(): ProofPath;
|
||||
private _createProofMap;
|
||||
private _buildTree;
|
||||
private _buildHashes;
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element): void;
|
||||
path(index: number): ProofPath;
|
||||
indexOf(element: Element, comparator?: <T>(arg0: T, arg1: T) => boolean): number;
|
||||
proof(element: Element): ProofPath;
|
||||
/**
|
||||
* Shifts edge of tree to left
|
||||
* @param edge new TreeEdge below current edge
|
||||
* @param elements leaves between old and new edge
|
||||
*/
|
||||
shiftEdge(edge: TreeEdge, elements: Element[]): void;
|
||||
serialize(): SerializedPartialTreeState;
|
||||
static deserialize(data: SerializedPartialTreeState, hashFunction?: HashFunction<Element>): PartialMerkleTree;
|
||||
toString(): string;
|
||||
}
|
||||
159
@tornado/fixed-merkle-tree/lib/PartialMerkleTree.js
Normal file
159
@tornado/fixed-merkle-tree/lib/PartialMerkleTree.js
Normal file
@@ -0,0 +1,159 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PartialMerkleTree = void 0;
|
||||
const simpleHash_1 = __importDefault(require("./simpleHash"));
|
||||
const BaseTree_1 = require("./BaseTree");
|
||||
class PartialMerkleTree extends BaseTree_1.BaseTree {
|
||||
constructor(levels, { edgePath, edgeElement, edgeIndex, edgeElementsCount, }, leaves, { hashFunction, zeroElement } = {}) {
|
||||
super();
|
||||
if (edgeIndex + leaves.length !== edgeElementsCount)
|
||||
throw new Error('Invalid number of elements');
|
||||
this._edgeLeafProof = edgePath;
|
||||
this._initialRoot = edgePath.pathRoot;
|
||||
this.zeroElement = zeroElement !== null && zeroElement !== void 0 ? zeroElement : 0;
|
||||
this._edgeLeaf = { data: edgeElement, index: edgeIndex };
|
||||
this._leavesAfterEdge = leaves;
|
||||
this.levels = levels;
|
||||
this._hashFn = hashFunction || simpleHash_1.default;
|
||||
this._createProofMap();
|
||||
this._buildTree();
|
||||
}
|
||||
get edgeIndex() {
|
||||
return this._edgeLeaf.index;
|
||||
}
|
||||
get edgeElement() {
|
||||
return this._edgeLeaf.data;
|
||||
}
|
||||
get edgeLeafProof() {
|
||||
return this._edgeLeafProof;
|
||||
}
|
||||
_createProofMap() {
|
||||
this._proofMap = this.edgeLeafProof.pathPositions.reduce((p, c, i) => {
|
||||
p.set(i, [c, this.edgeLeafProof.pathElements[i]]);
|
||||
return p;
|
||||
}, new Map());
|
||||
this._proofMap.set(this.levels, [0, this.edgeLeafProof.pathRoot]);
|
||||
}
|
||||
_buildTree() {
|
||||
const edgeLeafIndex = this._edgeLeaf.index;
|
||||
this._leaves = Array(edgeLeafIndex).concat(this._leavesAfterEdge);
|
||||
if (this._proofMap.has(0)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(0);
|
||||
this._leaves[proofPos] = proofEl;
|
||||
}
|
||||
this._layers = [this._leaves];
|
||||
this._buildZeros();
|
||||
this._buildHashes();
|
||||
}
|
||||
_buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1];
|
||||
const currentLayer = this._processNodes(nodes, layerIndex);
|
||||
if (this._proofMap.has(layerIndex)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(layerIndex);
|
||||
if (!currentLayer[proofPos])
|
||||
currentLayer[proofPos] = proofEl;
|
||||
}
|
||||
this._layers[layerIndex] = currentLayer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index, element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index);
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`);
|
||||
}
|
||||
this._layers[0][index] = element;
|
||||
this._processUpdate(index);
|
||||
}
|
||||
path(index) {
|
||||
var _a;
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index);
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`);
|
||||
}
|
||||
let elIndex = Number(index);
|
||||
const pathElements = [];
|
||||
const pathIndices = [];
|
||||
const pathPositions = [];
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2;
|
||||
const leafIndex = elIndex ^ 1;
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex];
|
||||
pathPositions[level] = leafIndex;
|
||||
}
|
||||
else {
|
||||
pathElements[level] = this._zeros[level];
|
||||
pathPositions[level] = 0;
|
||||
}
|
||||
const [proofPos, proofEl] = this._proofMap.get(level);
|
||||
pathElements[level] = (_a = pathElements[level]) !== null && _a !== void 0 ? _a : (proofPos === leafIndex ? proofEl : this._zeros[level]);
|
||||
elIndex >>= 1;
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
};
|
||||
}
|
||||
indexOf(element, comparator) {
|
||||
return BaseTree_1.BaseTree.indexOf(this._layers[0], element, this.edgeIndex, comparator);
|
||||
}
|
||||
proof(element) {
|
||||
const index = this.indexOf(element);
|
||||
return this.path(index);
|
||||
}
|
||||
/**
|
||||
* Shifts edge of tree to left
|
||||
* @param edge new TreeEdge below current edge
|
||||
* @param elements leaves between old and new edge
|
||||
*/
|
||||
shiftEdge(edge, elements) {
|
||||
if (this._edgeLeaf.index <= edge.edgeIndex) {
|
||||
throw new Error(`New edgeIndex should be smaller then ${this._edgeLeaf.index}`);
|
||||
}
|
||||
if (elements.length !== (this._edgeLeaf.index - edge.edgeIndex)) {
|
||||
throw new Error(`Elements length should be ${this._edgeLeaf.index - edge.edgeIndex}`);
|
||||
}
|
||||
this._edgeLeafProof = edge.edgePath;
|
||||
this._edgeLeaf = { index: edge.edgeIndex, data: edge.edgeElement };
|
||||
this._leavesAfterEdge = [...elements, ...this._leavesAfterEdge];
|
||||
this._createProofMap();
|
||||
this._buildTree();
|
||||
}
|
||||
serialize() {
|
||||
return {
|
||||
_edgeLeafProof: this._edgeLeafProof,
|
||||
_edgeLeaf: this._edgeLeaf,
|
||||
_layers: this._layers,
|
||||
_zeros: this._zeros,
|
||||
levels: this.levels,
|
||||
};
|
||||
}
|
||||
static deserialize(data, hashFunction) {
|
||||
const instance = Object.assign(Object.create(this.prototype), data);
|
||||
instance._hashFn = hashFunction || simpleHash_1.default;
|
||||
instance._initialRoot = data._edgeLeafProof.pathRoot;
|
||||
instance.zeroElement = instance._zeros[0];
|
||||
instance._leavesAfterEdge = instance._layers[0].slice(data._edgeLeaf.index);
|
||||
instance._createProofMap();
|
||||
return instance;
|
||||
}
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize());
|
||||
}
|
||||
}
|
||||
exports.PartialMerkleTree = PartialMerkleTree;
|
||||
45
@tornado/fixed-merkle-tree/lib/index.d.ts
vendored
Normal file
45
@tornado/fixed-merkle-tree/lib/index.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import { default as MerkleTree } from './FixedMerkleTree';
|
||||
export { PartialMerkleTree } from './PartialMerkleTree';
|
||||
export { simpleHash } from './simpleHash';
|
||||
export { MerkleTree };
|
||||
export default MerkleTree;
|
||||
export declare type HashFunction<T> = {
|
||||
(left: T, right: T): string;
|
||||
};
|
||||
export declare type MerkleTreeOptions = {
|
||||
hashFunction?: HashFunction<Element>;
|
||||
zeroElement?: Element;
|
||||
};
|
||||
export declare type Element = string | number;
|
||||
export declare type SerializedTreeState = {
|
||||
levels: number;
|
||||
_zeros: Array<Element>;
|
||||
_layers: Array<Element[]>;
|
||||
};
|
||||
export declare type SerializedPartialTreeState = {
|
||||
levels: number;
|
||||
_layers: Element[][];
|
||||
_zeros: Array<Element>;
|
||||
_edgeLeafProof: ProofPath;
|
||||
_edgeLeaf: LeafWithIndex;
|
||||
};
|
||||
export declare type ProofPath = {
|
||||
pathElements: Element[];
|
||||
pathIndices: number[];
|
||||
pathPositions: number[];
|
||||
pathRoot: Element;
|
||||
};
|
||||
export declare type TreeEdge = {
|
||||
edgeElement: Element;
|
||||
edgePath: ProofPath;
|
||||
edgeIndex: number;
|
||||
edgeElementsCount: number;
|
||||
};
|
||||
export declare type TreeSlice = {
|
||||
edge: TreeEdge;
|
||||
elements: Element[];
|
||||
};
|
||||
export declare type LeafWithIndex = {
|
||||
index: number;
|
||||
data: Element;
|
||||
};
|
||||
13
@tornado/fixed-merkle-tree/lib/index.js
Normal file
13
@tornado/fixed-merkle-tree/lib/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MerkleTree = exports.simpleHash = exports.PartialMerkleTree = void 0;
|
||||
const FixedMerkleTree_1 = __importDefault(require("./FixedMerkleTree"));
|
||||
Object.defineProperty(exports, "MerkleTree", { enumerable: true, get: function () { return FixedMerkleTree_1.default; } });
|
||||
var PartialMerkleTree_1 = require("./PartialMerkleTree");
|
||||
Object.defineProperty(exports, "PartialMerkleTree", { enumerable: true, get: function () { return PartialMerkleTree_1.PartialMerkleTree; } });
|
||||
var simpleHash_1 = require("./simpleHash");
|
||||
Object.defineProperty(exports, "simpleHash", { enumerable: true, get: function () { return simpleHash_1.simpleHash; } });
|
||||
exports.default = FixedMerkleTree_1.default;
|
||||
10
@tornado/fixed-merkle-tree/lib/simpleHash.d.ts
vendored
Normal file
10
@tornado/fixed-merkle-tree/lib/simpleHash.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Element } from './';
|
||||
/***
|
||||
* This is insecure hash function, just for example only
|
||||
* @param data
|
||||
* @param seed
|
||||
* @param hashLength
|
||||
*/
|
||||
export declare function simpleHash<T>(data: T[], seed?: number, hashLength?: number): string;
|
||||
declare const _default: (left: Element, right: Element) => string;
|
||||
export default _default;
|
||||
21
@tornado/fixed-merkle-tree/lib/simpleHash.js
Normal file
21
@tornado/fixed-merkle-tree/lib/simpleHash.js
Normal file
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.simpleHash = void 0;
|
||||
/***
|
||||
* This is insecure hash function, just for example only
|
||||
* @param data
|
||||
* @param seed
|
||||
* @param hashLength
|
||||
*/
|
||||
function simpleHash(data, seed, hashLength = 40) {
|
||||
const str = data.join('');
|
||||
let i, l, hval = seed !== null && seed !== void 0 ? seed : 0x811c9dcc5;
|
||||
for (i = 0, l = str.length; i < l; i++) {
|
||||
hval ^= str.charCodeAt(i);
|
||||
hval += (hval << 1) + (hval << 4) + (hval << 6) + (hval << 8) + (hval << 24);
|
||||
}
|
||||
const hash = (hval >>> 0).toString(16);
|
||||
return BigInt('0x' + hash.padEnd(hashLength - (hash.length - 1), '0')).toString(10);
|
||||
}
|
||||
exports.simpleHash = simpleHash;
|
||||
exports.default = (left, right) => simpleHash([left, right]);
|
||||
5411
@tornado/fixed-merkle-tree/package-lock.json
generated
5411
@tornado/fixed-merkle-tree/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"name": "@tornado/fixed-merkle-tree",
|
||||
"version": "0.6.1-p1",
|
||||
"version": "0.7.3-p0",
|
||||
"description": "Fixed depth merkle tree implementation with sequential inserts",
|
||||
"homepage": "https://git.tornado.ws/tornado-packages/archive-monorepo/src/branch/main/@tornado/fixed-merkle-tree",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tornadocash/fixed-merkle-tree.git"
|
||||
"url": "https://git.tornado.ws/tornado-packages/archive-monorepo"
|
||||
},
|
||||
"main": "index.js",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"lint": "eslint ."
|
||||
"test": "ts-mocha 'test/*.spec.ts' -s 10",
|
||||
"coverage": "nyc npm run test",
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf lib/",
|
||||
"prepare": "npm run clean && npm run build",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"keywords": [
|
||||
"merkle",
|
||||
@@ -18,15 +24,20 @@
|
||||
],
|
||||
"author": "Roman Semenov <semenov.roma@gmail.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@tornado/circomlib": "workspace:*",
|
||||
"@tornado/snarkjs": "workspace:*"
|
||||
},
|
||||
"files": [
|
||||
"src/*",
|
||||
"lib/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.5.0",
|
||||
"mocha": "^8.1.0",
|
||||
"typescript": "^5.0.2"
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"mocha": "^9.2.2",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
169
@tornado/fixed-merkle-tree/src/BaseTree.ts
Normal file
169
@tornado/fixed-merkle-tree/src/BaseTree.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Element, HashFunction, ProofPath } from './'
|
||||
|
||||
export class BaseTree {
|
||||
levels: number
|
||||
protected _hashFn: HashFunction<Element>
|
||||
protected zeroElement: Element
|
||||
protected _zeros: Element[]
|
||||
protected _layers: Array<Element[]>
|
||||
|
||||
get capacity() {
|
||||
return 2 ** this.levels
|
||||
}
|
||||
|
||||
get layers(): Array<Element[]> {
|
||||
return this._layers.slice()
|
||||
}
|
||||
|
||||
get zeros(): Element[] {
|
||||
return this._zeros.slice()
|
||||
}
|
||||
|
||||
get elements(): Element[] {
|
||||
return this._layers[0].slice()
|
||||
}
|
||||
|
||||
get root(): Element {
|
||||
return this._layers[this.levels][0] ?? this._zeros[this.levels]
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param elements elements of tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @param fromIndex The index to start the search at. If the index is greater than or equal to the array's length, -1 is returned
|
||||
* @returns {number} Index if element is found, otherwise -1
|
||||
*/
|
||||
static indexOf(elements: Element[], element: Element, fromIndex?: number, comparator?: <T> (arg0: T, arg1: T) => boolean): number {
|
||||
if (comparator) {
|
||||
return elements.findIndex((el) => comparator<Element>(element, el))
|
||||
} else {
|
||||
return elements.indexOf(element, fromIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element: Element) {
|
||||
if (this._layers[0].length >= this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this.update(this._layers[0].length, element)
|
||||
}
|
||||
|
||||
/*
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: Element[]): void {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i])
|
||||
let level = 0
|
||||
let index = this._layers[0].length - 1
|
||||
while (index % 2 === 1) {
|
||||
level++
|
||||
index >>= 1
|
||||
const left = this._layers[level - 1][index * 2]
|
||||
const right = this._layers[level - 1][index * 2 + 1]
|
||||
this._layers[level][index] = this._hashFn(left, right)
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1])
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index)
|
||||
}
|
||||
this._layers[0][index] = element
|
||||
this._processUpdate(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index: number): ProofPath {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index)
|
||||
}
|
||||
let elIndex = +index
|
||||
const pathElements: Element[] = []
|
||||
const pathIndices: number[] = []
|
||||
const pathPositions: number [] = []
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2
|
||||
const leafIndex = elIndex ^ 1
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex]
|
||||
pathPositions[level] = leafIndex
|
||||
} else {
|
||||
pathElements[level] = this._zeros[level]
|
||||
pathPositions[level] = 0
|
||||
}
|
||||
elIndex >>= 1
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
}
|
||||
}
|
||||
|
||||
protected _buildZeros() {
|
||||
this._zeros = [this.zeroElement]
|
||||
for (let i = 1; i <= this.levels; i++) {
|
||||
this._zeros[i] = this._hashFn(this._zeros[i - 1], this._zeros[i - 1])
|
||||
}
|
||||
}
|
||||
|
||||
protected _processNodes(nodes: Element[], layerIndex: number) {
|
||||
const length = nodes.length
|
||||
let currentLength = Math.ceil(length / 2)
|
||||
const currentLayer = new Array(currentLength)
|
||||
currentLength--
|
||||
const starFrom = length - ((length % 2) ^ 1)
|
||||
let j = 0
|
||||
for (let i = starFrom; i >= 0; i -= 2) {
|
||||
if (nodes[i - 1] === undefined) break
|
||||
const left = nodes[i - 1]
|
||||
const right = (i === starFrom && length % 2 === 1) ? this._zeros[layerIndex - 1] : nodes[i]
|
||||
currentLayer[currentLength - j] = this._hashFn(left, right)
|
||||
j++
|
||||
}
|
||||
return currentLayer
|
||||
}
|
||||
|
||||
protected _processUpdate(index: number) {
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
index >>= 1
|
||||
const left = this._layers[level - 1][index * 2]
|
||||
const right = index * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][index * 2 + 1]
|
||||
: this._zeros[level - 1]
|
||||
this._layers[level][index] = this._hashFn(left, right)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
128
@tornado/fixed-merkle-tree/src/FixedMerkleTree.ts
Normal file
128
@tornado/fixed-merkle-tree/src/FixedMerkleTree.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedTreeState, TreeEdge, TreeSlice } from './'
|
||||
import defaultHash from './simpleHash'
|
||||
import { BaseTree } from './BaseTree'
|
||||
|
||||
|
||||
export default class MerkleTree extends BaseTree {
|
||||
constructor(levels: number, elements: Element[] = [], {
|
||||
hashFunction = defaultHash,
|
||||
zeroElement = 0,
|
||||
}: MerkleTreeOptions = {}) {
|
||||
super()
|
||||
this.levels = levels
|
||||
if (elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this._hashFn = hashFunction
|
||||
this.zeroElement = zeroElement
|
||||
this._layers = []
|
||||
const leaves = elements.slice()
|
||||
this._layers = [leaves]
|
||||
this._buildZeros()
|
||||
this._buildHashes()
|
||||
}
|
||||
|
||||
private _buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1]
|
||||
this._layers[layerIndex] = this._processNodes(nodes, layerIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements: Element[]): void {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i])
|
||||
let level = 0
|
||||
let index = this._layers[0].length - 1
|
||||
while (index % 2 === 1) {
|
||||
level++
|
||||
index >>= 1
|
||||
this._layers[level][index] = this._hashFn(
|
||||
this._layers[level - 1][index * 2],
|
||||
this._layers[level - 1][index * 2 + 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1])
|
||||
}
|
||||
|
||||
indexOf(element: Element, comparator?: <T> (arg0: T, arg1: T) => boolean): number {
|
||||
return BaseTree.indexOf(this._layers[0], element, 0, comparator)
|
||||
}
|
||||
|
||||
proof(element: Element): ProofPath {
|
||||
const index = this.indexOf(element)
|
||||
return this.path(index)
|
||||
}
|
||||
|
||||
getTreeEdge(edgeIndex: number): TreeEdge {
|
||||
const edgeElement = this._layers[0][edgeIndex]
|
||||
if (edgeElement === undefined) {
|
||||
throw new Error('Element not found')
|
||||
}
|
||||
const edgePath = this.path(edgeIndex)
|
||||
return { edgePath, edgeElement, edgeIndex, edgeElementsCount: this._layers[0].length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 🪓
|
||||
* @param count
|
||||
*/
|
||||
getTreeSlices(count = 4): TreeSlice[] {
|
||||
const length = this._layers[0].length
|
||||
let size = Math.ceil(length / count)
|
||||
if (size % 2) size++
|
||||
const slices: TreeSlice[] = []
|
||||
for (let i = 0; i < length; i += size) {
|
||||
const edgeLeft = i
|
||||
const edgeRight = i + size
|
||||
slices.push({ edge: this.getTreeEdge(edgeLeft), elements: this.elements.slice(edgeLeft, edgeRight) })
|
||||
}
|
||||
return slices
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize(): SerializedTreeState {
|
||||
return {
|
||||
levels: this.levels,
|
||||
_zeros: this._zeros,
|
||||
_layers: this._layers,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*/
|
||||
static deserialize(data: SerializedTreeState, hashFunction?: HashFunction<Element>): MerkleTree {
|
||||
const instance: MerkleTree = Object.assign(Object.create(this.prototype), data)
|
||||
instance._hashFn = hashFunction || defaultHash
|
||||
instance.zeroElement = instance._zeros[0]
|
||||
return instance
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
186
@tornado/fixed-merkle-tree/src/PartialMerkleTree.ts
Normal file
186
@tornado/fixed-merkle-tree/src/PartialMerkleTree.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
Element,
|
||||
HashFunction,
|
||||
LeafWithIndex,
|
||||
MerkleTreeOptions,
|
||||
ProofPath,
|
||||
SerializedPartialTreeState,
|
||||
TreeEdge,
|
||||
} from './'
|
||||
import defaultHash from './simpleHash'
|
||||
import { BaseTree } from './BaseTree'
|
||||
|
||||
export class PartialMerkleTree extends BaseTree {
|
||||
private _leaves: Element[]
|
||||
private _leavesAfterEdge: Element[]
|
||||
private _edgeLeaf: LeafWithIndex
|
||||
private _initialRoot: Element
|
||||
private _edgeLeafProof: ProofPath
|
||||
private _proofMap: Map<number, [i: number, el: Element]>
|
||||
|
||||
constructor(levels: number, {
|
||||
edgePath,
|
||||
edgeElement,
|
||||
edgeIndex,
|
||||
edgeElementsCount,
|
||||
}: TreeEdge, leaves: Element[], { hashFunction, zeroElement }: MerkleTreeOptions = {}) {
|
||||
super()
|
||||
if (edgeIndex + leaves.length !== edgeElementsCount) throw new Error('Invalid number of elements')
|
||||
this._edgeLeafProof = edgePath
|
||||
this._initialRoot = edgePath.pathRoot
|
||||
this.zeroElement = zeroElement ?? 0
|
||||
this._edgeLeaf = { data: edgeElement, index: edgeIndex }
|
||||
this._leavesAfterEdge = leaves
|
||||
this.levels = levels
|
||||
this._hashFn = hashFunction || defaultHash
|
||||
this._createProofMap()
|
||||
this._buildTree()
|
||||
}
|
||||
|
||||
get edgeIndex(): number {
|
||||
return this._edgeLeaf.index
|
||||
}
|
||||
|
||||
get edgeElement(): Element {
|
||||
return this._edgeLeaf.data
|
||||
}
|
||||
|
||||
get edgeLeafProof(): ProofPath {
|
||||
return this._edgeLeafProof
|
||||
}
|
||||
|
||||
private _createProofMap() {
|
||||
this._proofMap = this.edgeLeafProof.pathPositions.reduce((p, c, i) => {
|
||||
p.set(i, [c, this.edgeLeafProof.pathElements[i]])
|
||||
return p
|
||||
}, new Map())
|
||||
this._proofMap.set(this.levels, [0, this.edgeLeafProof.pathRoot])
|
||||
}
|
||||
|
||||
private _buildTree(): void {
|
||||
const edgeLeafIndex = this._edgeLeaf.index
|
||||
this._leaves = Array(edgeLeafIndex).concat(this._leavesAfterEdge)
|
||||
if (this._proofMap.has(0)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(0)
|
||||
this._leaves[proofPos] = proofEl
|
||||
}
|
||||
this._layers = [this._leaves]
|
||||
this._buildZeros()
|
||||
this._buildHashes()
|
||||
}
|
||||
|
||||
private _buildHashes() {
|
||||
for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) {
|
||||
const nodes = this._layers[layerIndex - 1]
|
||||
const currentLayer = this._processNodes(nodes, layerIndex)
|
||||
if (this._proofMap.has(layerIndex)) {
|
||||
const [proofPos, proofEl] = this._proofMap.get(layerIndex)
|
||||
if (!currentLayer[proofPos]) currentLayer[proofPos] = proofEl
|
||||
}
|
||||
this._layers[layerIndex] = currentLayer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index: number, element: Element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index)
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`)
|
||||
}
|
||||
this._layers[0][index] = element
|
||||
this._processUpdate(index)
|
||||
}
|
||||
|
||||
path(index: number): ProofPath {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index)
|
||||
}
|
||||
if (index < this._edgeLeaf.index) {
|
||||
throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`)
|
||||
}
|
||||
let elIndex = Number(index)
|
||||
const pathElements: Element[] = []
|
||||
const pathIndices: number[] = []
|
||||
const pathPositions: number [] = []
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = elIndex % 2
|
||||
const leafIndex = elIndex ^ 1
|
||||
if (leafIndex < this._layers[level].length) {
|
||||
pathElements[level] = this._layers[level][leafIndex]
|
||||
pathPositions[level] = leafIndex
|
||||
} else {
|
||||
pathElements[level] = this._zeros[level]
|
||||
pathPositions[level] = 0
|
||||
}
|
||||
const [proofPos, proofEl] = this._proofMap.get(level)
|
||||
pathElements[level] = pathElements[level] ?? (proofPos === leafIndex ? proofEl : this._zeros[level])
|
||||
elIndex >>= 1
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
pathPositions,
|
||||
pathRoot: this.root,
|
||||
}
|
||||
}
|
||||
|
||||
indexOf(element: Element, comparator?: <T> (arg0: T, arg1: T) => boolean): number {
|
||||
return BaseTree.indexOf(this._layers[0], element, this.edgeIndex, comparator)
|
||||
}
|
||||
|
||||
proof(element: Element): ProofPath {
|
||||
const index = this.indexOf(element)
|
||||
return this.path(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts edge of tree to left
|
||||
* @param edge new TreeEdge below current edge
|
||||
* @param elements leaves between old and new edge
|
||||
*/
|
||||
|
||||
shiftEdge(edge: TreeEdge, elements: Element[]) {
|
||||
if (this._edgeLeaf.index <= edge.edgeIndex) {
|
||||
throw new Error(`New edgeIndex should be smaller then ${this._edgeLeaf.index}`)
|
||||
}
|
||||
if (elements.length !== (this._edgeLeaf.index - edge.edgeIndex)) {
|
||||
throw new Error(`Elements length should be ${this._edgeLeaf.index - edge.edgeIndex}`)
|
||||
}
|
||||
this._edgeLeafProof = edge.edgePath
|
||||
this._edgeLeaf = { index: edge.edgeIndex, data: edge.edgeElement }
|
||||
this._leavesAfterEdge = [...elements, ...this._leavesAfterEdge]
|
||||
this._createProofMap()
|
||||
this._buildTree()
|
||||
}
|
||||
|
||||
serialize(): SerializedPartialTreeState {
|
||||
return {
|
||||
_edgeLeafProof: this._edgeLeafProof,
|
||||
_edgeLeaf: this._edgeLeaf,
|
||||
_layers: this._layers,
|
||||
_zeros: this._zeros,
|
||||
levels: this.levels,
|
||||
}
|
||||
}
|
||||
|
||||
static deserialize(data: SerializedPartialTreeState, hashFunction?: HashFunction<Element>): PartialMerkleTree {
|
||||
const instance: PartialMerkleTree = Object.assign(Object.create(this.prototype), data)
|
||||
instance._hashFn = hashFunction || defaultHash
|
||||
instance._initialRoot = data._edgeLeafProof.pathRoot
|
||||
instance.zeroElement = instance._zeros[0]
|
||||
instance._leavesAfterEdge = instance._layers[0].slice(data._edgeLeaf.index)
|
||||
instance._createProofMap()
|
||||
return instance
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.serialize())
|
||||
}
|
||||
}
|
||||
46
@tornado/fixed-merkle-tree/src/index.ts
Normal file
46
@tornado/fixed-merkle-tree/src/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { default as MerkleTree } from './FixedMerkleTree'
|
||||
export { PartialMerkleTree } from './PartialMerkleTree'
|
||||
export { simpleHash } from './simpleHash'
|
||||
export { MerkleTree }
|
||||
export default MerkleTree
|
||||
export type HashFunction<T> = {
|
||||
(left: T, right: T): string
|
||||
}
|
||||
|
||||
export type MerkleTreeOptions = {
|
||||
hashFunction?: HashFunction<Element>
|
||||
zeroElement?: Element
|
||||
}
|
||||
|
||||
export type Element = string | number
|
||||
|
||||
export type SerializedTreeState = {
|
||||
levels: number,
|
||||
_zeros: Array<Element>,
|
||||
_layers: Array<Element[]>
|
||||
}
|
||||
|
||||
export type SerializedPartialTreeState = {
|
||||
levels: number
|
||||
_layers: Element[][]
|
||||
_zeros: Array<Element>
|
||||
_edgeLeafProof: ProofPath
|
||||
_edgeLeaf: LeafWithIndex
|
||||
}
|
||||
|
||||
export type ProofPath = {
|
||||
pathElements: Element[],
|
||||
pathIndices: number[],
|
||||
pathPositions: number[],
|
||||
pathRoot: Element
|
||||
}
|
||||
export type TreeEdge = {
|
||||
edgeElement: Element;
|
||||
edgePath: ProofPath;
|
||||
edgeIndex: number;
|
||||
edgeElementsCount: number;
|
||||
}
|
||||
|
||||
export type TreeSlice = { edge: TreeEdge, elements: Element[] }
|
||||
export type LeafWithIndex = { index: number, data: Element }
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
// keccak256("tornado") % BN254_FIELD_SIZE
|
||||
const DEFAULT_ZERO = '21663839004416932945382355908790599225266501822907911457504978515578255421292'
|
||||
const defaultHash = require('./mimc')
|
||||
|
||||
// todo ensure consistent types in tree and inserted elements?
|
||||
// todo make sha3 default hasher (and update tests) to get rid of mimc/snarkjs/circomlib dependency
|
||||
|
||||
/**
|
||||
* @callback hashFunction
|
||||
* @param left Left leaf
|
||||
* @param right Right leaf
|
||||
*/
|
||||
/**
|
||||
* Merkle tree
|
||||
*/
|
||||
class MerkleTree {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {number} levels Number of levels in the tree
|
||||
* @param {Array} [elements] Initial elements
|
||||
* @param {Object} options
|
||||
* @param {hashFunction} [options.hashFunction] Function used to hash 2 leaves
|
||||
* @param [options.zeroElement] Value for non-existent leaves
|
||||
*/
|
||||
constructor(levels, elements = [], { hashFunction, zeroElement = DEFAULT_ZERO } = {}) {
|
||||
this.levels = levels
|
||||
this.capacity = 2 ** levels
|
||||
if (elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this._hash = hashFunction || defaultHash
|
||||
this.zeroElement = zeroElement
|
||||
this._zeros = []
|
||||
this._zeros[0] = zeroElement
|
||||
for (let i = 1; i <= levels; i++) {
|
||||
this._zeros[i] = this._hash(this._zeros[i - 1], this._zeros[i - 1])
|
||||
}
|
||||
this._layers = []
|
||||
this._layers[0] = elements.slice()
|
||||
this._rebuild()
|
||||
}
|
||||
|
||||
_rebuild() {
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
this._layers[level] = []
|
||||
for (let i = 0; i < Math.ceil(this._layers[level - 1].length / 2); i++) {
|
||||
this._layers[level][i] = this._hash(
|
||||
this._layers[level - 1][i * 2],
|
||||
i * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][i * 2 + 1]
|
||||
: this._zeros[level - 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tree root
|
||||
* @returns {*}
|
||||
*/
|
||||
root() {
|
||||
return this._layers[this.levels].length > 0 ? this._layers[this.levels][0] : this._zeros[this.levels]
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new element into the tree
|
||||
* @param element Element to insert
|
||||
*/
|
||||
insert(element) {
|
||||
if (this._layers[0].length >= this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
this.update(this._layers[0].length, element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert multiple elements into the tree.
|
||||
* @param {Array} elements Elements to insert
|
||||
*/
|
||||
bulkInsert(elements) {
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._layers[0].length + elements.length > this.capacity) {
|
||||
throw new Error('Tree is full')
|
||||
}
|
||||
// First we insert all elements except the last one
|
||||
// updating only full subtree hashes (all layers where inserted element has odd index)
|
||||
// the last element will update the full path to the root making the tree consistent again
|
||||
for (let i = 0; i < elements.length - 1; i++) {
|
||||
this._layers[0].push(elements[i])
|
||||
let level = 0
|
||||
let index = this._layers[0].length - 1
|
||||
while (index % 2 === 1) {
|
||||
level++
|
||||
index >>= 1
|
||||
this._layers[level][index] = this._hash(
|
||||
this._layers[level - 1][index * 2],
|
||||
this._layers[level - 1][index * 2 + 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
this.insert(elements[elements.length - 1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an element in the tree
|
||||
* @param {number} index Index of element to change
|
||||
* @param element Updated element value
|
||||
*/
|
||||
update(index, element) {
|
||||
if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) {
|
||||
throw new Error('Insert index out of bounds: ' + index)
|
||||
}
|
||||
this._layers[0][index] = element
|
||||
for (let level = 1; level <= this.levels; level++) {
|
||||
index >>= 1
|
||||
this._layers[level][index] = this._hash(
|
||||
this._layers[level - 1][index * 2],
|
||||
index * 2 + 1 < this._layers[level - 1].length
|
||||
? this._layers[level - 1][index * 2 + 1]
|
||||
: this._zeros[level - 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merkle path to a leaf
|
||||
* @param {number} index Leaf index to generate path for
|
||||
* @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index
|
||||
*/
|
||||
path(index) {
|
||||
if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
|
||||
throw new Error('Index out of bounds: ' + index)
|
||||
}
|
||||
const pathElements = []
|
||||
const pathIndices = []
|
||||
for (let level = 0; level < this.levels; level++) {
|
||||
pathIndices[level] = index % 2
|
||||
pathElements[level] =
|
||||
(index ^ 1) < this._layers[level].length ? this._layers[level][index ^ 1] : this._zeros[level]
|
||||
index >>= 1
|
||||
}
|
||||
return {
|
||||
pathElements,
|
||||
pathIndices,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element in the tree
|
||||
* @param element An element to find
|
||||
* @param comparator A function that checks leaf value equality
|
||||
* @returns {number} Index if element is found, otherwise -1
|
||||
*/
|
||||
indexOf(element, comparator) {
|
||||
if (comparator) {
|
||||
return this._layers[0].findIndex((el) => comparator(element, el))
|
||||
} else {
|
||||
return this._layers[0].indexOf(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of non-zero tree elements
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
elements() {
|
||||
return this._layers[0].slice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of n-th zero elements array
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
zeros() {
|
||||
return this._zeros.slice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entire tree state including intermediate layers into a plain object
|
||||
* Deserializing it back will not require to recompute any hashes
|
||||
* Elements are not converted to a plain type, this is responsibility of the caller
|
||||
*/
|
||||
serialize() {
|
||||
return {
|
||||
levels: this.levels,
|
||||
_zeros: this._zeros,
|
||||
_layers: this._layers,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data into a MerkleTree instance
|
||||
* Make sure to provide the same hashFunction as was used in the source tree,
|
||||
* otherwise the tree state will be invalid
|
||||
*
|
||||
* @param data
|
||||
* @param hashFunction
|
||||
* @returns {MerkleTree}
|
||||
*/
|
||||
static deserialize(data, hashFunction) {
|
||||
const instance = Object.assign(Object.create(this.prototype), data)
|
||||
instance._hash = hashFunction || defaultHash
|
||||
instance.capacity = 2 ** instance.levels
|
||||
instance.zeroElement = instance._zeros[0]
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MerkleTree
|
||||
@@ -1,3 +0,0 @@
|
||||
const { mimcsponge } = require('circomlib')
|
||||
const { bigInt } = require('@tornado/snarkjs')
|
||||
module.exports = (left, right) => mimcsponge.multiHash([bigInt(left), bigInt(right)]).toString()
|
||||
22
@tornado/fixed-merkle-tree/src/simpleHash.ts
Normal file
22
@tornado/fixed-merkle-tree/src/simpleHash.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Element } from './'
|
||||
|
||||
/***
|
||||
* This is insecure hash function, just for example only
|
||||
* @param data
|
||||
* @param seed
|
||||
* @param hashLength
|
||||
*/
|
||||
|
||||
export function simpleHash<T>(data: T[], seed?: number, hashLength = 40): string {
|
||||
const str = data.join('')
|
||||
let i, l,
|
||||
hval = seed ?? 0x811c9dcc5
|
||||
for (i = 0, l = str.length; i < l; i++) {
|
||||
hval ^= str.charCodeAt(i)
|
||||
hval += (hval << 1) + (hval << 4) + (hval << 6) + (hval << 8) + (hval << 24)
|
||||
}
|
||||
const hash = (hval >>> 0).toString(16)
|
||||
return BigInt('0x' + hash.padEnd(hashLength - (hash.length - 1), '0')).toString(10)
|
||||
}
|
||||
|
||||
export default (left: Element, right: Element): string => simpleHash([left, right])
|
||||
428
@tornado/fixed-merkle-tree/test/fixedMerkleTree.spec.ts
Normal file
428
@tornado/fixed-merkle-tree/test/fixedMerkleTree.spec.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
import { MerkleTree, PartialMerkleTree, TreeEdge } from '../src'
|
||||
import { assert, should } from 'chai'
|
||||
import { createHash } from 'crypto'
|
||||
import { it } from 'mocha'
|
||||
|
||||
const sha256Hash = (left, right) => createHash('sha256').update(`${left}${right}`).digest('hex')
|
||||
const ZERO_ELEMENT = '21663839004416932945382355908790599225266501822907911457504978515578255421292'
|
||||
|
||||
describe('MerkleTree', () => {
|
||||
|
||||
describe('#constructor', () => {
|
||||
|
||||
it('should have correct zero root', () => {
|
||||
const tree = new MerkleTree(10, [])
|
||||
return should().equal(tree.root, '3060353338620102847451617558650138132480')
|
||||
})
|
||||
|
||||
it('should have correct 1 element root', () => {
|
||||
const tree = new MerkleTree(10, [1])
|
||||
should().equal(tree.root, '4059654748770657324723044385589999697920')
|
||||
})
|
||||
|
||||
it('should have correct even elements root', () => {
|
||||
const tree = new MerkleTree(10, [1, 2])
|
||||
should().equal(tree.root, '3715471817149864798706576217905179918336')
|
||||
})
|
||||
|
||||
it('should have correct odd elements root', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3])
|
||||
should().equal(tree.root, '5199180210167621115778229238102210117632')
|
||||
})
|
||||
|
||||
it('should be able to create a full tree', () => {
|
||||
new MerkleTree(2, [1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should fail to create tree with too many elements', () => {
|
||||
const call = () => new MerkleTree(2, [1, 2, 3, 4, 5])
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
|
||||
it('should work with optional hash function and zero element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5, 6], { hashFunction: sha256Hash, zeroElement: 'zero' })
|
||||
should().equal(tree.root, 'a377b9fa0ed41add83e56f7e1d0e2ebdb46550b9d8b26b77dece60cb67283f19')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#insert', () => {
|
||||
it('should insert into empty tree', () => {
|
||||
const tree = new MerkleTree(10)
|
||||
tree.insert(42)
|
||||
should().equal(tree.root, '750572848877730275626358141391262973952')
|
||||
})
|
||||
|
||||
it('should insert into odd tree', () => {
|
||||
const tree = new MerkleTree(10, [1])
|
||||
tree.insert(42)
|
||||
should().equal(tree.root, '5008383558940708447763798816817296703488')
|
||||
})
|
||||
|
||||
it('should insert into even tree', () => {
|
||||
const tree = new MerkleTree(10, [1, 2])
|
||||
tree.insert(42)
|
||||
should().equal(tree.root, '5005864318873356880627322373636156817408')
|
||||
})
|
||||
|
||||
it('should insert last element', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3])
|
||||
tree.insert(4)
|
||||
})
|
||||
|
||||
it('should fail to insert when tree is full', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3, 4])
|
||||
const call = () => tree.insert(5)
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#bulkInsert', () => {
|
||||
it('should work', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3])
|
||||
tree.bulkInsert([4, 5, 6])
|
||||
should().equal(tree.root, '4066635800770511602067209448381558554624')
|
||||
})
|
||||
|
||||
it('should give the same result as sequential inserts', () => {
|
||||
const initialArray = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 2, 3],
|
||||
[1, 2, 3, 4],
|
||||
]
|
||||
const insertedArray = [
|
||||
[11],
|
||||
[11, 12],
|
||||
[11, 12, 13],
|
||||
[11, 12, 13, 14],
|
||||
]
|
||||
for (const initial of initialArray) {
|
||||
for (const inserted of insertedArray) {
|
||||
const tree1 = new MerkleTree(10, initial)
|
||||
const tree2 = new MerkleTree(10, initial)
|
||||
tree1.bulkInsert(inserted)
|
||||
for (const item of inserted) {
|
||||
tree2.insert(item)
|
||||
}
|
||||
should().equal(tree1.root, tree2.root)
|
||||
}
|
||||
}
|
||||
}).timeout(10000)
|
||||
|
||||
it('should work with max elements', () => {
|
||||
const tree = new MerkleTree(2, [1, 2])
|
||||
tree.bulkInsert([3, 4])
|
||||
})
|
||||
|
||||
it('should fail to insert too many elements', () => {
|
||||
const tree = new MerkleTree(2, [1, 2])
|
||||
const call = () => tree.bulkInsert([3, 4, 5])
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
|
||||
it('should bypass empty elements', () => {
|
||||
const elements = [1, 2, 3, 4]
|
||||
const tree = new MerkleTree(2, elements)
|
||||
tree.bulkInsert([])
|
||||
assert.deepEqual(tree.elements, elements, 'No elements inserted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#update', () => {
|
||||
it('should update first element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(0, 42)
|
||||
should().equal(tree.root, '3884161948856565981263417078389340635136')
|
||||
})
|
||||
|
||||
it('should update last element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(4, 42)
|
||||
should().equal(tree.root, '3564959811529894228734180300843252711424')
|
||||
})
|
||||
|
||||
it('should update odd element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(1, 42)
|
||||
should().equal(tree.root, '4576704573778433422699674477203122290688')
|
||||
})
|
||||
|
||||
it('should update even element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
tree.update(2, 42)
|
||||
should().equal(tree.root, '1807994110952186123819489133812038762496')
|
||||
})
|
||||
|
||||
it('should update extra element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4])
|
||||
tree.update(4, 5)
|
||||
should().equal(tree.root, '1099080610107164849381389194938128793600')
|
||||
})
|
||||
|
||||
it('should fail to update incorrect index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().throw((() => tree.update(-1, 42)), 'Insert index out of bounds: -1')
|
||||
should().throw((() => tree.update(6, 42)), 'Insert index out of bounds: 6')
|
||||
// @ts-ignore
|
||||
should().throw((() => tree.update('qwe', 42)), 'Insert index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail to update over capacity', () => {
|
||||
const tree = new MerkleTree(2, [1, 2, 3, 4])
|
||||
const call = () => tree.update(4, 42)
|
||||
should().throw(call, 'Insert index out of bounds: 4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#indexOf', () => {
|
||||
it('should find index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().equal(tree.indexOf(3), 2)
|
||||
})
|
||||
|
||||
it('should work with comparator', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().equal(tree.indexOf(4, (arg0, arg1) => arg0 === arg1), 3)
|
||||
})
|
||||
|
||||
it('should return -1 for non existent element', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
should().equal(tree.indexOf(42), -1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#path', () => {
|
||||
it('should work for even index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path(2)
|
||||
assert.deepEqual(path.pathIndices, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
assert.deepEqual(path.pathElements, [
|
||||
4,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'3591172241203040147397382471352592629760',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456',
|
||||
|
||||
])
|
||||
})
|
||||
|
||||
it('should work for odd index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
const path = tree.path(3)
|
||||
assert.deepEqual(path.pathIndices, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
assert.deepEqual(path.pathElements, [
|
||||
3,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'3591172241203040147397382471352592629760',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456',
|
||||
])
|
||||
})
|
||||
|
||||
it('should fail on incorrect index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4])
|
||||
should().throw((() => tree.path(-1)), 'Index out of bounds: -1')
|
||||
should().throw((() => tree.path(5)), 'Index out of bounds: 5')
|
||||
// @ts-ignore
|
||||
should().throw((() => tree.path('qwe')), 'Index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should work for correct string index', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
// @ts-ignore
|
||||
const path = tree.path('2')
|
||||
assert.deepEqual(path.pathIndices, [0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
assert.deepEqual(path.pathElements, [
|
||||
4,
|
||||
'4027992409016347597424110157229339967488',
|
||||
'3591172241203040147397382471352592629760',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
'2074434463882483178614385966084599578624',
|
||||
'2808856778596740691845240322870189490176',
|
||||
'4986731814143931240516913804278285467648',
|
||||
'1918547053077726613961101558405545328640',
|
||||
'5444383861051812288142814494928935059456',
|
||||
|
||||
])
|
||||
})
|
||||
})
|
||||
describe('#proof', () => {
|
||||
it('should return proof for leaf', () => {
|
||||
const tree = new MerkleTree(10, [1, 2, 3, 4, 5])
|
||||
assert.deepEqual(tree.proof(4), tree.path(3))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTreeEdge', () => {
|
||||
it('should return correct treeEdge', () => {
|
||||
const expectedEdge: TreeEdge = {
|
||||
edgePath: {
|
||||
pathElements: [
|
||||
5,
|
||||
'1390935134112885103361924701261056180224',
|
||||
'1952916572242076545231119328171167580160',
|
||||
'938972308169430750202858820582946897920',
|
||||
],
|
||||
pathIndices: [0, 0, 1, 0],
|
||||
pathPositions: [5, 0, 0, 0],
|
||||
pathRoot: '3283298202329284319899364273680487022592',
|
||||
},
|
||||
edgeElement: 4,
|
||||
edgeIndex: 4,
|
||||
edgeElementsCount: 6,
|
||||
}
|
||||
const tree = new MerkleTree(4, [0, 1, 2, 3, 4, 5])
|
||||
assert.deepEqual(tree.getTreeEdge(4), expectedEdge)
|
||||
})
|
||||
it('should fail if element not found', () => {
|
||||
const tree = new MerkleTree(4, [0, 1, 2, 3, 4, 5])
|
||||
const call = () => tree.getTreeEdge(6)
|
||||
should().throw(call, 'Element not found')
|
||||
})
|
||||
})
|
||||
describe('#getTreeSlices', () => {
|
||||
let fullTree: MerkleTree
|
||||
before(async () => {
|
||||
const elements = Array.from({ length: 2 ** 10 }, (_, i) => i)
|
||||
fullTree = new MerkleTree(10, elements)
|
||||
return Promise.resolve()
|
||||
})
|
||||
it('should return correct slices count', () => {
|
||||
const count = 4
|
||||
const slicesCount = fullTree.getTreeSlices(4).length
|
||||
should().equal(count, slicesCount)
|
||||
}).timeout(10000)
|
||||
|
||||
it('should be able to create partial tree from last slice', () => {
|
||||
const [, , , lastSlice] = fullTree.getTreeSlices()
|
||||
const partialTree = new PartialMerkleTree(10, lastSlice.edge, lastSlice.elements)
|
||||
assert.deepEqual(fullTree.root, partialTree.root)
|
||||
}).timeout(10000)
|
||||
|
||||
it('should be able to build full tree from slices', () => {
|
||||
const slices = fullTree.getTreeSlices()
|
||||
const lastSlice = slices.pop()
|
||||
const partialTree = new PartialMerkleTree(10, lastSlice.edge, lastSlice.elements)
|
||||
slices.reverse().forEach(({ edge, elements }) => {
|
||||
partialTree.shiftEdge(edge, elements)
|
||||
})
|
||||
assert.deepEqual(fullTree.layers, partialTree.layers)
|
||||
}).timeout(10000)
|
||||
|
||||
it('should return same path', () => {
|
||||
const slices = fullTree.getTreeSlices()
|
||||
const lastSlice = slices.pop()
|
||||
const partialTree = new PartialMerkleTree(10, lastSlice.edge, lastSlice.elements)
|
||||
slices.reverse().forEach(({ edge, elements }) => {
|
||||
partialTree.shiftEdge(edge, elements)
|
||||
})
|
||||
assert.deepEqual(fullTree.path(100), partialTree.path(100))
|
||||
}).timeout(10000)
|
||||
|
||||
it('should throw if invalid number of elements', () => {
|
||||
const [firstSlice] = fullTree.getTreeSlices()
|
||||
const call = () => new PartialMerkleTree(10, firstSlice.edge, firstSlice.elements)
|
||||
should().throw(call, 'Invalid number of elements')
|
||||
}).timeout(10000)
|
||||
})
|
||||
describe('#getters', () => {
|
||||
const elements = [1, 2, 3, 4, 5]
|
||||
const layers = [
|
||||
[1, 2, 3, 4, 5],
|
||||
[
|
||||
'4027992409016347597424110157229339967488',
|
||||
'923221781152860005594997320673730232320',
|
||||
'752191049236692618445397735417537626112',
|
||||
|
||||
],
|
||||
[
|
||||
'81822854828781486047086122479545722339328',
|
||||
'3591172241203040147397382471352592629760',
|
||||
|
||||
],
|
||||
['2729943778107054496417267081388406865920'],
|
||||
['4562739390655416913642128116127918718976'],
|
||||
]
|
||||
|
||||
it('should return same elements in array', () => {
|
||||
const tree = new MerkleTree(10, elements)
|
||||
assert.deepEqual(tree.elements, elements)
|
||||
})
|
||||
it('should return copy of elements array', () => {
|
||||
const tree = new MerkleTree(10, elements)
|
||||
const elements1 = tree.elements
|
||||
tree.insert(6)
|
||||
const elements2 = tree.elements
|
||||
should().not.equal(elements1, elements2)
|
||||
})
|
||||
|
||||
it('should return same layers in array', () => {
|
||||
const tree = new MerkleTree(4, elements)
|
||||
assert.deepEqual(tree.layers, layers)
|
||||
})
|
||||
it('should return copy of elements array', () => {
|
||||
const tree = new MerkleTree(4, elements)
|
||||
const layers1 = tree.layers
|
||||
tree.insert(6)
|
||||
const layers2 = tree.layers
|
||||
should().not.equal(layers1, layers2)
|
||||
})
|
||||
it('should return correct zeros array', () => {
|
||||
const zeros = [
|
||||
0,
|
||||
'1390935134112885103361924701261056180224',
|
||||
'3223901263414086620636498663535535980544',
|
||||
'938972308169430750202858820582946897920',
|
||||
'3743880566844110745576746962917825445888',
|
||||
]
|
||||
const tree = new MerkleTree(4, [])
|
||||
assert.deepEqual(tree.zeros, zeros, 'Not equal')
|
||||
})
|
||||
it('should return copy of zeros array', () => {
|
||||
const tree = new MerkleTree(4, [])
|
||||
const zeros1 = tree.zeros
|
||||
tree.insert(6)
|
||||
const zeros2 = tree.zeros
|
||||
should().not.equal(zeros1, zeros2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#serialize', () => {
|
||||
it('should work', () => {
|
||||
const src = new MerkleTree(10, [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
const data = src.serialize()
|
||||
const dst = MerkleTree.deserialize(data)
|
||||
should().equal(src.root, dst.root)
|
||||
|
||||
src.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
should().equal(src.root, dst.root)
|
||||
})
|
||||
})
|
||||
describe('#toString', () => {
|
||||
it('should return correct stringified representation', () => {
|
||||
const src = new MerkleTree(10, [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
const str = src.toString()
|
||||
const dst = MerkleTree.deserialize(JSON.parse(str))
|
||||
should().equal(src.root, dst.root)
|
||||
|
||||
src.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
should().equal(src.root, dst.root)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
297
@tornado/fixed-merkle-tree/test/partialMerkleTree.spec.ts
Normal file
297
@tornado/fixed-merkle-tree/test/partialMerkleTree.spec.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { Element, MerkleTree, MerkleTreeOptions, PartialMerkleTree } from '../src'
|
||||
import { it } from 'mocha'
|
||||
import { should } from 'chai'
|
||||
import * as assert from 'assert'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
const sha256Hash = (left, right) => createHash('sha256').update(`${left}${right}`).digest('hex')
|
||||
|
||||
describe('PartialMerkleTree', () => {
|
||||
const getTestTrees = (levels: number, elements: Element[], edgeIndex: number, treeOptions: MerkleTreeOptions = {}) => {
|
||||
const fullTree = new MerkleTree(levels, elements, treeOptions)
|
||||
const edge = fullTree.getTreeEdge(edgeIndex)
|
||||
const leavesAfterEdge = elements.slice(edge.edgeIndex)
|
||||
const partialTree = new PartialMerkleTree(levels, edge, leavesAfterEdge, treeOptions)
|
||||
return { fullTree, partialTree }
|
||||
}
|
||||
describe('#constructor', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(20, ['0', '1', '2', '3', '4', '5'], 2)
|
||||
it('should initialize merkle tree with same root', () => {
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should initialize merkle tree with same leaves count', () => {
|
||||
should().equal(fullTree.elements.length, partialTree.elements.length)
|
||||
})
|
||||
|
||||
it('should work with optional hash function and zero element', () => {
|
||||
const { partialTree, fullTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6], 3, {
|
||||
hashFunction: sha256Hash,
|
||||
zeroElement: 'zero',
|
||||
})
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#insert', () => {
|
||||
|
||||
it('should have equal root to full tree after insertion ', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, ['0', '1', '2', '3', '4', '5', '6', '7'], 5)
|
||||
fullTree.insert('9')
|
||||
partialTree.insert('9')
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should fail to insert when tree is full', () => {
|
||||
const { partialTree } = getTestTrees(3, ['0', '1', '2', '3', '4', '5', '6', '7'], 5)
|
||||
const call = () => partialTree.insert('8')
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#bulkInsert', () => {
|
||||
|
||||
it('should work like full tree', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(20, [1, 2, 3, 4, 5], 2)
|
||||
partialTree.bulkInsert([6, 7, 8])
|
||||
fullTree.bulkInsert([6, 7, 8])
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should give the same result as sequential inserts', () => {
|
||||
const initialArray = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 2, 3],
|
||||
[1, 2, 3, 4],
|
||||
]
|
||||
const insertedArray = [
|
||||
[11],
|
||||
[11, 12],
|
||||
[11, 12, 13],
|
||||
[11, 12, 13, 14],
|
||||
]
|
||||
for (const initial of initialArray) {
|
||||
for (const inserted of insertedArray) {
|
||||
const { partialTree: tree1 } = getTestTrees(10, initial, initial.length - 1)
|
||||
const { partialTree: tree2 } = getTestTrees(10, initial, initial.length - 1)
|
||||
tree1.bulkInsert(inserted)
|
||||
for (const item of inserted) {
|
||||
tree2.insert(item)
|
||||
}
|
||||
should().equal(tree1.root, tree2.root)
|
||||
}
|
||||
}
|
||||
}).timeout(10000)
|
||||
|
||||
it('should fail to insert too many elements', () => {
|
||||
const { partialTree } = getTestTrees(2, [1, 2, 3, 4], 2)
|
||||
const call = () => partialTree.bulkInsert([5, 6, 7])
|
||||
should().throw(call, 'Tree is full')
|
||||
})
|
||||
it('should bypass empty elements', () => {
|
||||
const elements = [1, 2, 3, 4]
|
||||
const { partialTree } = getTestTrees(2, elements, 2)
|
||||
partialTree.bulkInsert([])
|
||||
should().equal(partialTree.elements.length, elements.length, 'No elements inserted')
|
||||
})
|
||||
})
|
||||
describe('#update', () => {
|
||||
it('should update last element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
partialTree.update(4, 42)
|
||||
fullTree.update(4, 42)
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
|
||||
|
||||
it('should update odd element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8], 2)
|
||||
partialTree.update(4, 42)
|
||||
fullTree.update(4, 42)
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
|
||||
it('should update even element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8], 2)
|
||||
partialTree.update(3, 42)
|
||||
fullTree.update(3, 42)
|
||||
should().equal(partialTree.root, fullTree.root)
|
||||
})
|
||||
|
||||
it('should update extra element', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
partialTree.update(5, 6)
|
||||
fullTree.update(5, 6)
|
||||
should().equal(fullTree.root, partialTree.root)
|
||||
})
|
||||
|
||||
it('should fail to update incorrect index', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 3)
|
||||
should().throw((() => partialTree.update(-1, 42)), 'Insert index out of bounds: -1')
|
||||
should().throw((() => partialTree.update(6, 42)), 'Insert index out of bounds: 6')
|
||||
should().throw((() => partialTree.update(2, 42)), 'Index 2 is below the edge: 3')
|
||||
// @ts-ignore
|
||||
should().throw((() => partialTree.update('qwe', 42)), 'Insert index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail to update over capacity', () => {
|
||||
const { partialTree } = getTestTrees(2, [1, 2, 3, 4], 1)
|
||||
const call = () => partialTree.update(4, 42)
|
||||
should().throw(call, 'Insert index out of bounds: 4')
|
||||
})
|
||||
})
|
||||
describe('#indexOf', () => {
|
||||
it('should return same result as full tree', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8], 3)
|
||||
should().equal(partialTree.indexOf(5), fullTree.indexOf(5))
|
||||
})
|
||||
|
||||
it('should find index', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.indexOf(3), 2)
|
||||
})
|
||||
|
||||
it('should work with comparator', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.indexOf(4, (arg0, arg1) => arg0 === arg1), 3)
|
||||
})
|
||||
|
||||
it('should return -1 for non existent element', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.indexOf(42), -1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#proof', () => {
|
||||
it('should return proof for known leaf', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
assert.deepEqual(partialTree.proof(4), partialTree.path(3))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getters', () => {
|
||||
it('should return capacity', () => {
|
||||
const levels = 10
|
||||
const capacity = 2 ** levels
|
||||
const { fullTree, partialTree } = getTestTrees(levels, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(fullTree.capacity, capacity)
|
||||
should().equal(partialTree.capacity, capacity)
|
||||
})
|
||||
|
||||
it('should return same elements count as full tree', () => {
|
||||
const levels = 10
|
||||
const capacity = 2 ** levels
|
||||
const elements = Array.from({ length: capacity }, (_, i) => i)
|
||||
const { fullTree, partialTree } = getTestTrees(levels, elements, 200)
|
||||
should().equal(partialTree.elements.length, fullTree.elements.length)
|
||||
})
|
||||
|
||||
it('should return copy of layers', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
const layers = partialTree.layers
|
||||
should().not.equal(layers, partialTree.layers)
|
||||
})
|
||||
|
||||
it('should return copy of zeros', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
const zeros = partialTree.zeros
|
||||
should().not.equal(zeros, partialTree.zeros)
|
||||
})
|
||||
|
||||
it('should return edge leaf', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5], 2)
|
||||
should().equal(partialTree.edgeElement, 3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#path', () => {
|
||||
|
||||
it('should return path for known nodes', () => {
|
||||
const levels = 10
|
||||
const capacity = 2 ** levels
|
||||
const elements = Array.from({ length: capacity / 2 }, (_, i) => i)
|
||||
const { fullTree, partialTree } = getTestTrees(levels, elements, 250)
|
||||
assert.deepEqual(fullTree.path(251), partialTree.path(251))
|
||||
}).timeout(1000)
|
||||
|
||||
it('should fail on incorrect index', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
should().throw((() => partialTree.path(-1)), 'Index out of bounds: -1')
|
||||
should().throw((() => partialTree.path(10)), 'Index out of bounds: 10')
|
||||
// @ts-ignore
|
||||
should().throw((() => partialTree.path('qwe')), 'Index out of bounds: qwe')
|
||||
})
|
||||
|
||||
it('should fail if index is below edge', () => {
|
||||
const { partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
const call = () => partialTree.path(2)
|
||||
should().throw(call, 'Index 2 is below the edge: 4')
|
||||
})
|
||||
})
|
||||
describe('#shiftEdge', () => {
|
||||
const levels = 20
|
||||
const elements: Element[] = Array.from({ length: 2 ** 18 }, (_, i) => i)
|
||||
const tree = new MerkleTree(levels, elements)
|
||||
it('should work', () => {
|
||||
const edge1 = tree.getTreeEdge(200)
|
||||
const edge2 = tree.getTreeEdge(100)
|
||||
const partialTree = new PartialMerkleTree(levels, edge1, elements.slice(edge1.edgeIndex))
|
||||
partialTree.shiftEdge(edge2, elements.slice(edge2.edgeIndex, partialTree.edgeIndex))
|
||||
tree.insert('1111')
|
||||
partialTree.insert('1111')
|
||||
assert.deepEqual(partialTree.path(150), tree.path(150))
|
||||
})
|
||||
it('should be able to build full tree from slices', () => {
|
||||
const slices = tree.getTreeSlices(6)
|
||||
const lastSlice = slices.pop()
|
||||
const partialTree = new PartialMerkleTree(levels, lastSlice.edge, lastSlice.elements)
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
partialTree.shiftEdge(slices[i].edge, slices[i].elements)
|
||||
}
|
||||
partialTree.insert('1')
|
||||
tree.insert('1')
|
||||
assert.deepStrictEqual(partialTree.path(432), tree.path(432))
|
||||
}).timeout(10000)
|
||||
|
||||
it('should fail if new edge index is over current edge', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
const newEdge = fullTree.getTreeEdge(4)
|
||||
const call = () => partialTree.shiftEdge(newEdge, [1, 2])
|
||||
should().throw(call, 'New edgeIndex should be smaller then 4')
|
||||
})
|
||||
it('should fail if elements length are incorrect', () => {
|
||||
const { fullTree, partialTree } = getTestTrees(10, [1, 2, 3, 4, 5, 6, 7, 8, 9], 4)
|
||||
const newEdge = fullTree.getTreeEdge(3)
|
||||
const call = () => partialTree.shiftEdge(newEdge, [1, 2])
|
||||
should().throw(call, 'Elements length should be 1')
|
||||
})
|
||||
})
|
||||
describe('#serialize', () => {
|
||||
it('should work', () => {
|
||||
const { partialTree } = getTestTrees(5, [1, 2, 3, 4, 5, 6, 7, 8, 9], 5)
|
||||
const data = partialTree.serialize()
|
||||
const dst = PartialMerkleTree.deserialize(data)
|
||||
should().equal(partialTree.root, dst.root)
|
||||
|
||||
partialTree.insert(10)
|
||||
dst.insert(10)
|
||||
assert.deepStrictEqual(partialTree.path(6), dst.path(6))
|
||||
should().equal(partialTree.root, dst.root)
|
||||
})
|
||||
})
|
||||
describe('#toString', () => {
|
||||
it('should return correct stringified representation', () => {
|
||||
const { partialTree } = getTestTrees(5, [1, 2, 3, 4, 5, 6, 7, 8, 9], 5)
|
||||
const str = partialTree.toString()
|
||||
const dst = PartialMerkleTree.deserialize(JSON.parse(str))
|
||||
assert.deepStrictEqual(partialTree.path(6), dst.path(6))
|
||||
partialTree.insert(10)
|
||||
dst.insert(10)
|
||||
|
||||
assert.deepStrictEqual(partialTree.path(6), dst.path(6))
|
||||
assert.deepStrictEqual(partialTree.root, dst.root)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
18
@tornado/fixed-merkle-tree/test/simpleHash.spec.ts
Normal file
18
@tornado/fixed-merkle-tree/test/simpleHash.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { it } from 'mocha'
|
||||
import { should } from 'chai'
|
||||
import { simpleHash } from '../src'
|
||||
|
||||
describe('SimpleHash', () => {
|
||||
it('should return correct hash string with default params', () => {
|
||||
const hash = simpleHash([1, 2, 3])
|
||||
return should().equal(hash, '3530513397947785053296897142557895557120')
|
||||
})
|
||||
it('should return correct hash string with length param', () => {
|
||||
const hash = simpleHash([1, 2, 3], null, 77)
|
||||
return should().equal(hash, '1259729275322113643079999203492506359813191573070980317691663537897682854338069790720')
|
||||
})
|
||||
it('should return correct hash string with seed param', () => {
|
||||
const hash = simpleHash(['1', '2', '3'], 123)
|
||||
return should().equal(hash, '1371592418687375416654554138100746944512')
|
||||
})
|
||||
})
|
||||
@@ -1,22 +1,25 @@
|
||||
{
|
||||
// Change this to match your project
|
||||
"include": ["src/**/*", "index.js"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
// Tells TypeScript to read JS files, as
|
||||
// normally they are ignored as source files
|
||||
"allowJs": true,
|
||||
// Generate d.ts files
|
||||
"declaration": true,
|
||||
// This compiler run should
|
||||
// only output d.ts files
|
||||
"emitDeclarationOnly": true,
|
||||
// Types should go into this directory.
|
||||
// Removing this would place the .d.ts files
|
||||
// next to the .js files
|
||||
"outFile": "index.d.ts",
|
||||
// go to js file when using IDE functions like
|
||||
// "Go to Definition" in VSCode
|
||||
"declarationMap": true
|
||||
}
|
||||
}
|
||||
"lib": [
|
||||
"es2020"
|
||||
],
|
||||
"target": "es2017",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"types",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
||||
13
@tornado/gas-price-oracle/.editiorconfig
Normal file
13
@tornado/gas-price-oracle/.editiorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
38
@tornado/gas-price-oracle/.eslintrc
Normal file
38
@tornado/gas-price-oracle/.eslintrc
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"plugins": ["prettier", "eslint-plugin-import", "eslint-plugin-node", "eslint-plugin-promise", "@typescript-eslint"],
|
||||
"extends": ["prettier", "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"project": "./tsconfig.json",
|
||||
"sourceType": "module",
|
||||
"target": "esnext",
|
||||
"module": "commonjs"
|
||||
},
|
||||
"rules": {
|
||||
"node/no-callback-literal": 0,
|
||||
"prettier/prettier": ["error", { "printWidth": 130 }],
|
||||
"jsx-quotes": ["error", "prefer-single"],
|
||||
"@typescript-eslint/restrict-template-expressions": ["warn", { "allowNumber": true }],
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/array-type": 2,
|
||||
"@typescript-eslint/camelcase": 0,
|
||||
"@typescript-eslint/consistent-type-definitions": 0,
|
||||
"@typescript-eslint/no-empty-function": 2,
|
||||
"@typescript-eslint/no-empty-interface": 2,
|
||||
"@typescript-eslint/no-explicit-any": 2,
|
||||
"@typescript-eslint/no-extra-non-null-assertion": 2,
|
||||
"@typescript-eslint/require-await": 2,
|
||||
"@typescript-eslint/no-floating-promises": 0,
|
||||
"@typescript-eslint/strict-boolean-expressions": 0
|
||||
}
|
||||
}
|
||||
60
@tornado/gas-price-oracle/.github/workflows/nodejs.yml
vendored
Normal file
60
@tornado/gas-price-oracle/.github/workflows/nodejs.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn test
|
||||
- run: yarn lint
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- run: yarn install
|
||||
- name: NPM login
|
||||
# NPM doesn't understand env vars and needs auth file lol
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Set vars
|
||||
id: vars
|
||||
run: |
|
||||
echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
echo "::set-output name=repo_name::$(echo ${GITHUB_REPOSITORY#*/})"
|
||||
- name: Check package.json version vs tag
|
||||
run: |
|
||||
[ ${{ steps.vars.outputs.version }} = $(grep '"version":' package.json | grep -o "[0-9.]*") ] || (echo "Git tag doesn't match version in package.json" && false)
|
||||
- name: Publish to npm
|
||||
run: npm publish
|
||||
- name: Create GitHub Release Draft
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.vars.outputs.version }}
|
||||
draft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Telegram Message Notify
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
message: 🚀 Published [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) version [${{ steps.vars.outputs.version }}](https://www.npmjs.com/package/${{ steps.vars.outputs.repo_name }}/v/${{ steps.vars.outputs.version }}) to npm
|
||||
debug: true
|
||||
format: markdown
|
||||
91
@tornado/gas-price-oracle/.gitignore
vendored
Normal file
91
@tornado/gas-price-oracle/.gitignore
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
1
@tornado/gas-price-oracle/.nvmrc
Normal file
1
@tornado/gas-price-oracle/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
16.15.1
|
||||
1
@tornado/gas-price-oracle/.prettierignore
Normal file
1
@tornado/gas-price-oracle/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
lib
|
||||
8
@tornado/gas-price-oracle/.prettierrc
Normal file
8
@tornado/gas-price-oracle/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 130,
|
||||
"tabWidth": 2
|
||||
}
|
||||
21
@tornado/gas-price-oracle/LICENSE
Normal file
21
@tornado/gas-price-oracle/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 PepperSec
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
315
@tornado/gas-price-oracle/README.md
Normal file
315
@tornado/gas-price-oracle/README.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# Gas Price Oracle library for Ethereum dApps [](https://github.com/peppersec/gas-price-oracle/actions) [](https://www.npmjs.com/package/gas-price-oracle)
|
||||
|
||||
This is a library with a collection of onchain and offchain gas price oracle URLs
|
||||
|
||||
## Supported networks
|
||||
|
||||
### Ethereum Mainnet
|
||||
|
||||
Current offchain list:
|
||||
|
||||
- https://etherchain.org/api/gasnow
|
||||
|
||||
Current onchain list:
|
||||
|
||||
- [Chainlink aggregator](https://etherscan.io/address/0x169e633a2d1e6c10dd91238ba11c4a708dfef37c#readContract)
|
||||
|
||||
### Binance Smart Chain
|
||||
|
||||
Current offchain list:
|
||||
|
||||
- https://ztake.org/
|
||||
|
||||
### Gnosis Chain
|
||||
|
||||
Current offchain list:
|
||||
|
||||
- https://gnosis.blockscout.com/api/v1/gas-price-oracle
|
||||
|
||||
### Polygon (Matic) Network
|
||||
|
||||
Current offchain list:
|
||||
|
||||
- https://gasstation.polygon.technology/v2
|
||||
- https://matic-gas-station.tornado.ws
|
||||
|
||||
### Avalanche C Network
|
||||
|
||||
Current offchain list:
|
||||
|
||||
- https://gavax.blockscan.com/gasapi.ashx?apikey=key&method=gasoracle
|
||||
|
||||
## Installation
|
||||
|
||||
`npm i gas-price-oracle`
|
||||
or
|
||||
`yarn add gas-price-oracle`
|
||||
|
||||
## Import
|
||||
|
||||
```js
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
or
|
||||
import { GasPriceOracle } from 'gas-price-oracle'
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
type GasPrice = Record<'instant' | 'fast' | 'standard' | 'low', number>
|
||||
|
||||
type EstimatedGasPrice = {
|
||||
maxFeePerGas: number
|
||||
baseFee: number | undefined
|
||||
maxPriorityFeePerGas: number
|
||||
}
|
||||
|
||||
type FallbackGasPrices = {
|
||||
gasPrices?: GasPrice
|
||||
estimated?: EstimatedGasPrice
|
||||
}
|
||||
|
||||
type GasOracleOptions = {
|
||||
chainId?: number
|
||||
timeout?: number
|
||||
defaultRpc?: string
|
||||
blocksCount?: number
|
||||
percentile?: number
|
||||
blockTime?: number // seconds
|
||||
shouldCache?: boolean
|
||||
fallbackGasPrices?: FallbackGasPrices
|
||||
}
|
||||
|
||||
const options: GasOracleOptions = {
|
||||
chainId: 1,
|
||||
percentile: 5, // Which percentile of effective priority fees to include
|
||||
blocksCount: 10, // How many blocks to consider for priority fee estimation
|
||||
defaultRpc: 'https://api.mycryptoapi.com/eth',
|
||||
blockTime: 10, // seconds
|
||||
shouldCache: false,
|
||||
timeout: 10000, // specifies the number of milliseconds before the request times out.
|
||||
minPriority: 0, // specifies the min maxPriorityFeePerGas.
|
||||
fallbackGasPrices: {
|
||||
gasPrices: {
|
||||
instant: 28,
|
||||
fast: 22,
|
||||
standard: 17,
|
||||
low: 11,
|
||||
},
|
||||
estimated: {
|
||||
maxFeePerGas: 20,
|
||||
maxPriorityFeePerGas: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### The Oracle can cache rpc calls
|
||||
|
||||
For caching needs to provide to GasOracleOptions
|
||||
|
||||
`shouldCache: true`
|
||||
|
||||
`blockTime: <Chain block time duration>`
|
||||
|
||||
### EIP-1559 (estimated) gasPrice only
|
||||
|
||||
```typescript
|
||||
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||
|
||||
type EstimatedGasPrice = {
|
||||
maxFeePerGas: number
|
||||
baseFee: number | undefined
|
||||
maxPriorityFeePerGas: number
|
||||
}
|
||||
|
||||
fallbackGasPrices: EstimatedGasPrice = {
|
||||
maxFeePerGas: 20,
|
||||
maxPriorityFeePerGas: 3,
|
||||
}
|
||||
|
||||
oracle.eip1559.estimateFees(fallbackGasPrices).then((gasPrices: EstimatedGasPrice) => {
|
||||
console.log(gasPrices) // { baseFee: 14, maxFeePerGas: 17, maxPriorityFeePerGas: 3 }
|
||||
})
|
||||
```
|
||||
|
||||
### Legacy gasPrice only
|
||||
|
||||
```typescript
|
||||
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||
|
||||
type GasPrice = Record<'instant' | 'fast' | 'standard' | 'low', number>
|
||||
|
||||
fallbackGasPrices: GasPrice = {
|
||||
instant: 28,
|
||||
fast: 22,
|
||||
standard: 17,
|
||||
low: 11,
|
||||
}
|
||||
|
||||
oracle.legacy.gasPrices(fallbackGasPrices).then((gasPrices: GasPrice) => {
|
||||
console.log(gasPrices) // { instant: 21.5, fast: 19, standard: 17, low: 15 }
|
||||
})
|
||||
```
|
||||
|
||||
The `oracle.legacy.gasPrices` method also accepts `shouldGetMedian` argument (`true`) by default. For more details see [below](#offchain-oracles-only-get-median-price).
|
||||
Under the hood it's a combination of `fetchMedianGasPriceOffChain`(`fetchGasPricesOffChain`) and `fetchGasPricesOnChain` methods.
|
||||
|
||||
### Estimated gasPrices (EIP-1559) and Legacy gasPrice
|
||||
|
||||
```ts
|
||||
const oracle = new GasPriceOracle(options)
|
||||
|
||||
type GasPriceWithEstimate = {
|
||||
gasPrices: GasPrice
|
||||
estimate: EstimatedGasPrice
|
||||
}
|
||||
|
||||
type GasPricesWithEstimateInput = {
|
||||
shouldGetMedian?: boolean
|
||||
fallbackGasPrices?: FallbackGasPrices
|
||||
}
|
||||
|
||||
// optional fallbackGasPrices
|
||||
const fallbackGasPrices: FallbackGasPrices = {
|
||||
gasPrices: {
|
||||
instant: 28,
|
||||
fast: 22,
|
||||
standard: 17,
|
||||
low: 11,
|
||||
},
|
||||
estimated: {
|
||||
maxFeePerGas: 20,
|
||||
maxPriorityFeePerGas: 3,
|
||||
},
|
||||
}
|
||||
|
||||
oracle.gasPricesWithEstimate({ fallbackGasPrices, shouldGetMedian: true }).then((gasPrices: GasPriceWithEstimate) => {
|
||||
console.log(gasPrices) // {
|
||||
// estimated: { baseFee: 14, maxFeePerGas: 17, maxPriorityFeePerGas: 3 },
|
||||
// gasPrices: { instant: 21.5, fast: 19, standard: 17, low: 15 }
|
||||
// }}
|
||||
})
|
||||
```
|
||||
|
||||
### Estimated gasPrices (EIP-1559) or Legacy gasPrice
|
||||
|
||||
```typescript
|
||||
const oracle = new GasPriceOracle(options)
|
||||
|
||||
type GetGasPriceInput = {
|
||||
isLegacy?: boolean
|
||||
shouldGetMedian?: boolean
|
||||
fallbackGasPrices?: GasPrice
|
||||
}
|
||||
|
||||
// optional fallbackGasPrices
|
||||
const fallbackGasPrices: FallbackGasPrices = {
|
||||
gasPrices: {
|
||||
instant: 28,
|
||||
fast: 22,
|
||||
standard: 17,
|
||||
low: 11,
|
||||
},
|
||||
estimated: {
|
||||
maxFeePerGas: 20,
|
||||
maxPriorityFeePerGas: 3,
|
||||
},
|
||||
}
|
||||
|
||||
oracle.gasPrices({ fallbackGasPrices, shouldGetMedian: true }).then((gasPrices: GasPrice | EstimatedGasPrice) => {
|
||||
console.log(gasPrices) // {
|
||||
// baseFee: 14, maxFeePerGas: 17, maxPriorityFeePerGas: 3 ||
|
||||
// instant: 21.5, fast: 19, standard: 17, low: 15
|
||||
// }}
|
||||
})
|
||||
```
|
||||
|
||||
The `gasPrices` method also accepts `isLegacy` argument (`false`) by default. If `isLegacy: true` - `legacy gasPrice` will be provided. If the `estimate Gas` crashes, `legacy gas Price` will be provided.
|
||||
|
||||
### Get transaction gasPrice params
|
||||
|
||||
```typescript
|
||||
const oracle = new GasPriceOracle(options)
|
||||
|
||||
type GetTxGasParamsInput = {
|
||||
bumpPercent?: number
|
||||
legacySpeed?: GasPriceKey
|
||||
isLegacy?: boolean
|
||||
shouldGetMedian?: boolean
|
||||
fallbackGasPrices?: FallbackGasPrices
|
||||
}
|
||||
|
||||
type GetTxGasParamsRes =
|
||||
| {
|
||||
gasPrice: number
|
||||
}
|
||||
| {
|
||||
maxFeePerGas: number
|
||||
maxPriorityFeePerGas: number
|
||||
}
|
||||
|
||||
const gasParams: GetTxGasParamsRes = await oracle.getTxGasParams({ legacySpeed: 'fast', bumpPercent: 30 })
|
||||
console.log(gasParams)
|
||||
// { maxFeePerGas: '3f5476a00', maxPriorityFeePerGas: 'b2d05e00' } || { gasPrice: '46c7cfe00' }
|
||||
// equal to: { maxFeePerGas: 17 gwei, maxPriorityFeePerGas: 3 gwei } || { gasPrice: 19 gwei }
|
||||
|
||||
web3.eth.sendTransaction({
|
||||
from: '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8',
|
||||
to: '0xac03bb73b6a9e108530aff4df5077c2b3d481e5a',
|
||||
nonce: '0',
|
||||
gasLimit: '21000',
|
||||
value: '10000000000',
|
||||
...gasParams,
|
||||
})
|
||||
```
|
||||
|
||||
`bumpPercent` argument (`0` by default) - response data will increase by `bumpPercent`%.
|
||||
`legacySpeed` argument (`fast` by default) - select the speed of legacy gasPrice.
|
||||
|
||||
Returns gas info in `wei`, hex-format.
|
||||
|
||||
### Offchain oracles only
|
||||
|
||||
```typescript
|
||||
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||
|
||||
// shouldGetMedian: boolean | undefined
|
||||
oracle.legacy.fetchGasPricesOffChain((shouldGetMedian = true)).then((gasPrices: GasPrice) => {
|
||||
console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 }
|
||||
})
|
||||
```
|
||||
|
||||
### Offchain oracles only (get median price)
|
||||
|
||||
```typescript
|
||||
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||
|
||||
oracle.legacy.fetchMedianGasPriceOffChain().then((gasPrices: GasPrice) => {
|
||||
console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 }
|
||||
})
|
||||
```
|
||||
|
||||
This command provides the median gas price of all configured oracles.
|
||||
|
||||
### Custom RPC URL for onchain oracles
|
||||
|
||||
```typescript
|
||||
const defaultRpc = 'https://mainnet.infura.io/v3/<API_KEY>'
|
||||
const oracle = new GasPriceOracle({ defaultRpc, chainId: 1 })
|
||||
|
||||
oracle.legacy.fetchGasPricesOnChain().then((gasPrices: number) => {
|
||||
console.log(gasPrices) // 21
|
||||
})
|
||||
```
|
||||
|
||||
To get gasPrices from a chain outside of the application's chain list (Binance, Gnosis, Polygon, Avalanche), you should enter the rpcUrl into initial GasPriceOracle options\_
|
||||
|
||||
```typescript
|
||||
const defaultRpc = 'https://rpc.goerli.mudit.blog/' // goerli public rpcUrl
|
||||
const oracle = new GasPriceOracle({ defaultRpc, chainId: 5 })
|
||||
|
||||
oracle.gasPrices()
|
||||
```
|
||||
66
@tornado/gas-price-oracle/package.json
Normal file
66
@tornado/gas-price-oracle/package.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "@tornado/gas-price-oracle",
|
||||
"version": "0.5.3",
|
||||
"description": "Gas Price Oracle library for Ethereum dApps.",
|
||||
"homepage": "https://git.tornado.ws/tornado-packages/gas-price-oracle",
|
||||
"main": "./lib/index.js",
|
||||
"module": "./lib/esm/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.tornado.ws/tornado-packages/gas-price-oracle"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "ts-mocha --timeout 30000 --paths 'src/tests/*.test.ts'",
|
||||
"build": "tsc && tsc-alias",
|
||||
"build:esm": "tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json",
|
||||
"eslint": "eslint 'src/*.ts'",
|
||||
"prettier:check": "prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||
"lint": "yarn eslint && yarn prettier:check",
|
||||
"prepare": "yarn build && yarn build:esm",
|
||||
"prepublishOnly": "yarn test && yarn lint"
|
||||
},
|
||||
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
|
||||
"keywords": [
|
||||
"Gas",
|
||||
"Gas price",
|
||||
"Ethereum",
|
||||
"Oracle",
|
||||
"EIP-1559",
|
||||
"London Fork"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.11",
|
||||
"@types/chai-as-promised": "^7.1.3",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/mockery": "^1.4.29",
|
||||
"@types/node": "^14.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.1",
|
||||
"@typescript-eslint/parser": "^4.4.1",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.13.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"mocha": "^7.2.0",
|
||||
"mockery": "^2.1.0",
|
||||
"prettier": "^2.1.2",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^8.10.1",
|
||||
"tsc-alias": "^1.6.11",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.2",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"node-cache": "^5.1.2"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
]
|
||||
}
|
||||
10
@tornado/gas-price-oracle/src/config/arbitrum.ts
Normal file
10
@tornado/gas-price-oracle/src/config/arbitrum.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { OffChainOracles, OnChainOracles } from '@/services'
|
||||
|
||||
export const offChainOracles: OffChainOracles = {}
|
||||
|
||||
export const onChainOracles: OnChainOracles = {}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
23
@tornado/gas-price-oracle/src/config/avax.ts
Normal file
23
@tornado/gas-price-oracle/src/config/avax.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||
|
||||
const avalancheGasStation: OffChainOracle = {
|
||||
name: 'avalancheGasStation',
|
||||
url: 'https://gavax.blockscan.com/gasapi.ashx?apikey=key&method=gasoracle',
|
||||
instantPropertyName: 'FastGasPrice',
|
||||
fastPropertyName: 'FastGasPrice',
|
||||
standardPropertyName: 'ProposeGasPrice',
|
||||
lowPropertyName: 'SafeGasPrice',
|
||||
denominator: 1,
|
||||
additionalDataProperty: 'result',
|
||||
}
|
||||
|
||||
export const offChainOracles: OffChainOracles = {
|
||||
avalancheGasStation,
|
||||
}
|
||||
|
||||
export const onChainOracles: OnChainOracles = {}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
23
@tornado/gas-price-oracle/src/config/bsc.ts
Normal file
23
@tornado/gas-price-oracle/src/config/bsc.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||
|
||||
const ztake: OffChainOracle = {
|
||||
name: 'ztake',
|
||||
url: 'https://blockchains.ztake.org/api/h6WnmwNqw9CAJHzej5W4gD6LZ9n7v8EK/gasprice/bsc/',
|
||||
instantPropertyName: 'percentile_60',
|
||||
fastPropertyName: 'percentile_50',
|
||||
standardPropertyName: 'percentile_40',
|
||||
lowPropertyName: 'percentile_30',
|
||||
denominator: 1,
|
||||
additionalDataProperty: null,
|
||||
}
|
||||
|
||||
export const offChainOracles: OffChainOracles = {
|
||||
ztake,
|
||||
}
|
||||
|
||||
export const onChainOracles: OnChainOracles = {}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
78
@tornado/gas-price-oracle/src/config/index.ts
Normal file
78
@tornado/gas-price-oracle/src/config/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import bscOracles from './bsc'
|
||||
import xdaiOracles from './xdai'
|
||||
import avalancheOracles from './avax'
|
||||
import mainnetOracles from './mainnet'
|
||||
import polygonOracles from './polygon'
|
||||
import optimismOracles from './optimism'
|
||||
import arbitrumOracles from './arbitrum'
|
||||
|
||||
import { NetworksConfig } from '@/types'
|
||||
|
||||
export enum ChainId {
|
||||
MAINNET = 1,
|
||||
BSC = 56,
|
||||
XDAI = 100,
|
||||
POLYGON = 137,
|
||||
OPTIMISM = 10,
|
||||
ARBITRUM = 42161,
|
||||
AVAX = 43114,
|
||||
}
|
||||
|
||||
export const NETWORKS: Record<number, NetworksConfig> = {
|
||||
[ChainId.MAINNET]: {
|
||||
oracles: mainnetOracles,
|
||||
rpcUrl: 'https://api.mycryptoapi.com/eth',
|
||||
defaultGasPrice: 22,
|
||||
maxGasPrice: 1500,
|
||||
blocksCount: 10,
|
||||
percentile: 5,
|
||||
},
|
||||
[ChainId.BSC]: {
|
||||
oracles: bscOracles,
|
||||
rpcUrl: 'https://bsc-dataseed1.ninicoin.io',
|
||||
defaultGasPrice: 5,
|
||||
maxGasPrice: 200,
|
||||
blocksCount: 10,
|
||||
percentile: 5,
|
||||
},
|
||||
[ChainId.XDAI]: {
|
||||
oracles: xdaiOracles,
|
||||
rpcUrl: 'https://rpc.gnosischain.com',
|
||||
defaultGasPrice: 5,
|
||||
maxGasPrice: 200,
|
||||
blocksCount: 200,
|
||||
percentile: 5,
|
||||
},
|
||||
[ChainId.POLYGON]: {
|
||||
oracles: polygonOracles,
|
||||
rpcUrl: 'https://rpc-mainnet.maticvigil.com',
|
||||
defaultGasPrice: 75,
|
||||
maxGasPrice: 1000,
|
||||
blocksCount: 10,
|
||||
percentile: 5,
|
||||
},
|
||||
[ChainId.OPTIMISM]: {
|
||||
oracles: optimismOracles,
|
||||
rpcUrl: 'https://mainnet.optimism.io',
|
||||
defaultGasPrice: 0.001,
|
||||
maxGasPrice: 5,
|
||||
blocksCount: 10,
|
||||
percentile: 5,
|
||||
},
|
||||
[ChainId.ARBITRUM]: {
|
||||
oracles: arbitrumOracles,
|
||||
rpcUrl: 'https://arb1.arbitrum.io/rpc',
|
||||
defaultGasPrice: 3,
|
||||
maxGasPrice: 15,
|
||||
blocksCount: 10,
|
||||
percentile: 5,
|
||||
},
|
||||
[ChainId.AVAX]: {
|
||||
oracles: avalancheOracles,
|
||||
rpcUrl: 'https://api.avax.network/ext/bc/C/rpc',
|
||||
defaultGasPrice: 50,
|
||||
maxGasPrice: 1000,
|
||||
blocksCount: 10,
|
||||
percentile: 5,
|
||||
},
|
||||
}
|
||||
32
@tornado/gas-price-oracle/src/config/mainnet.ts
Normal file
32
@tornado/gas-price-oracle/src/config/mainnet.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { OffChainOracle, OffChainOracles, OnChainOracle, OnChainOracles } from '@/services'
|
||||
|
||||
const etherchain: OffChainOracle = {
|
||||
name: 'etherchain',
|
||||
url: 'https://etherchain.org/api/gasnow',
|
||||
instantPropertyName: 'rapid',
|
||||
fastPropertyName: 'fast',
|
||||
standardPropertyName: 'standard',
|
||||
lowPropertyName: 'slow',
|
||||
denominator: 1e9,
|
||||
additionalDataProperty: 'data',
|
||||
}
|
||||
|
||||
const chainlink: OnChainOracle = {
|
||||
name: 'chainlink',
|
||||
callData: '0x50d25bcd',
|
||||
contract: '0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C',
|
||||
denominator: '1000000000',
|
||||
}
|
||||
|
||||
export const offChainOracles: OffChainOracles = {
|
||||
etherchain,
|
||||
}
|
||||
|
||||
export const onChainOracles: OnChainOracles = {
|
||||
chainlink,
|
||||
}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
18
@tornado/gas-price-oracle/src/config/optimism.ts
Normal file
18
@tornado/gas-price-oracle/src/config/optimism.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { OffChainOracles, OnChainOracle, OnChainOracles } from '@/services'
|
||||
|
||||
export const offChainOracles: OffChainOracles = {}
|
||||
|
||||
const optimism: OnChainOracle = {
|
||||
name: 'optimism',
|
||||
callData: '0xfe173b97',
|
||||
denominator: '1000000000',
|
||||
contract: '0x420000000000000000000000000000000000000F',
|
||||
}
|
||||
export const onChainOracles: OnChainOracles = {
|
||||
optimism,
|
||||
}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
35
@tornado/gas-price-oracle/src/config/polygon.ts
Normal file
35
@tornado/gas-price-oracle/src/config/polygon.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||
|
||||
const maticGasStation: OffChainOracle = {
|
||||
name: 'maticGasStation',
|
||||
url: 'https://gasstation.polygon.technology/v2',
|
||||
instantPropertyName: 'fast.maxFee',
|
||||
fastPropertyName: 'fast.maxFee',
|
||||
standardPropertyName: 'standard.maxFee',
|
||||
lowPropertyName: 'safeLow.maxFee',
|
||||
denominator: 1,
|
||||
additionalDataProperty: null,
|
||||
}
|
||||
|
||||
const tornadoMaticGasStation: OffChainOracle = {
|
||||
name: 'tornadoMGasStation',
|
||||
url: 'https://matic-gas-station.tornado.ws',
|
||||
instantPropertyName: 'standard',
|
||||
fastPropertyName: 'standard',
|
||||
standardPropertyName: 'safeLow',
|
||||
lowPropertyName: 'safeLow',
|
||||
denominator: 1,
|
||||
additionalDataProperty: null,
|
||||
}
|
||||
|
||||
export const offChainOracles: OffChainOracles = {
|
||||
maticGasStation,
|
||||
tornadoMaticGasStation,
|
||||
}
|
||||
|
||||
export const onChainOracles: OnChainOracles = {}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
23
@tornado/gas-price-oracle/src/config/xdai.ts
Normal file
23
@tornado/gas-price-oracle/src/config/xdai.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||
|
||||
const blockscout: OffChainOracle = {
|
||||
name: 'blockscout',
|
||||
url: 'https://gnosis.blockscout.com/api/v1/gas-price-oracle',
|
||||
instantPropertyName: 'fast',
|
||||
fastPropertyName: 'average',
|
||||
standardPropertyName: 'slow',
|
||||
lowPropertyName: 'slow',
|
||||
denominator: 1,
|
||||
additionalDataProperty: null,
|
||||
}
|
||||
|
||||
export const offChainOracles: OffChainOracles = {
|
||||
blockscout,
|
||||
}
|
||||
|
||||
export const onChainOracles: OnChainOracles = {}
|
||||
|
||||
export default {
|
||||
offChainOracles,
|
||||
onChainOracles,
|
||||
}
|
||||
27
@tornado/gas-price-oracle/src/constants/index.ts
Normal file
27
@tornado/gas-price-oracle/src/constants/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
const GWEI = 1e9
|
||||
const DEFAULT_TIMEOUT = 10000
|
||||
|
||||
const ROUND_UP = 1
|
||||
const ROUND_DOWN = 2
|
||||
const GWEI_PRECISION = 9
|
||||
const INT_PRECISION = 0
|
||||
|
||||
const SUCCESS_STATUS = 200
|
||||
const BG_ZERO = new BigNumber(0)
|
||||
const PERCENT_MULTIPLIER = 100
|
||||
const DEFAULT_BLOCK_DURATION = 10
|
||||
|
||||
export {
|
||||
GWEI,
|
||||
DEFAULT_TIMEOUT,
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
GWEI_PRECISION,
|
||||
INT_PRECISION,
|
||||
SUCCESS_STATUS,
|
||||
BG_ZERO,
|
||||
PERCENT_MULTIPLIER,
|
||||
DEFAULT_BLOCK_DURATION,
|
||||
}
|
||||
1
@tornado/gas-price-oracle/src/index.ts
Normal file
1
@tornado/gas-price-oracle/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@/services/gas-price-oracle'
|
||||
20
@tornado/gas-price-oracle/src/services/cacher/cacheNode.ts
Normal file
20
@tornado/gas-price-oracle/src/services/cacher/cacheNode.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import NodeCache, { Options } from 'node-cache'
|
||||
|
||||
export class NodeJSCache<T> {
|
||||
private nodeCache: NodeCache
|
||||
constructor(params: Options) {
|
||||
this.nodeCache = new NodeCache(params)
|
||||
}
|
||||
|
||||
async get(key: string): Promise<T | undefined> {
|
||||
return await this.nodeCache.get<T>(key)
|
||||
}
|
||||
|
||||
async set(key: string, value: T): Promise<boolean> {
|
||||
return await this.nodeCache.set(key, value)
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return await this.nodeCache.has(key)
|
||||
}
|
||||
}
|
||||
1
@tornado/gas-price-oracle/src/services/cacher/index.ts
Normal file
1
@tornado/gas-price-oracle/src/services/cacher/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './cacheNode'
|
||||
@@ -0,0 +1,26 @@
|
||||
// How many blocks to consider for priority fee estimation
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { EstimatedGasPrice } from './types'
|
||||
|
||||
const FEE_HISTORY_BLOCKS = 10
|
||||
// Which percentile of effective priority fees to include
|
||||
const FEE_HISTORY_PERCENTILE = 5
|
||||
|
||||
const DEFAULT_BASE_FEE = 20
|
||||
const DEFAULT_PRIORITY_FEE = 3
|
||||
const PRIORITY_FEE_INCREASE_BOUNDARY = 200 // %
|
||||
|
||||
const FALLBACK_ESTIMATE: EstimatedGasPrice = {
|
||||
baseFee: DEFAULT_BASE_FEE,
|
||||
maxPriorityFeePerGas: DEFAULT_PRIORITY_FEE,
|
||||
maxFeePerGas: new BigNumber(DEFAULT_PRIORITY_FEE).plus(DEFAULT_BASE_FEE).toNumber(),
|
||||
}
|
||||
|
||||
export {
|
||||
DEFAULT_BASE_FEE,
|
||||
FALLBACK_ESTIMATE,
|
||||
FEE_HISTORY_BLOCKS,
|
||||
DEFAULT_PRIORITY_FEE,
|
||||
FEE_HISTORY_PERCENTILE,
|
||||
PRIORITY_FEE_INCREASE_BOUNDARY,
|
||||
}
|
||||
159
@tornado/gas-price-oracle/src/services/gas-estimation/eip1559.ts
Normal file
159
@tornado/gas-price-oracle/src/services/gas-estimation/eip1559.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import { FeeHistory, Block } from '@/types'
|
||||
import { Config, EstimateOracle, EstimatedGasPrice, CalculateFeesParams, GasEstimationOptionsPayload } from './types'
|
||||
|
||||
import { ChainId, NETWORKS } from '@/config'
|
||||
import { RpcFetcher, NodeJSCache } from '@/services'
|
||||
import { findMax, fromNumberToHex, fromWeiToGwei, getMedian } from '@/utils'
|
||||
import { BG_ZERO, DEFAULT_BLOCK_DURATION, PERCENT_MULTIPLIER } from '@/constants'
|
||||
|
||||
import { DEFAULT_PRIORITY_FEE, PRIORITY_FEE_INCREASE_BOUNDARY, FEE_HISTORY_BLOCKS, FEE_HISTORY_PERCENTILE } from './constants'
|
||||
|
||||
// !!! MAKE SENSE ALL CALCULATIONS IN GWEI !!!
|
||||
export class Eip1559GasPriceOracle implements EstimateOracle {
|
||||
public configuration: Config = {
|
||||
shouldCache: false,
|
||||
chainId: ChainId.MAINNET,
|
||||
fallbackGasPrices: undefined,
|
||||
minPriority: DEFAULT_PRIORITY_FEE,
|
||||
blockTime: DEFAULT_BLOCK_DURATION,
|
||||
blocksCount: NETWORKS[ChainId.MAINNET].blocksCount,
|
||||
percentile: NETWORKS[ChainId.MAINNET].percentile,
|
||||
}
|
||||
private fetcher: RpcFetcher
|
||||
|
||||
private cache: NodeJSCache<EstimatedGasPrice>
|
||||
private FEES_KEY = (chainId: ChainId) => `estimate-fee-${chainId}`
|
||||
|
||||
constructor({ fetcher, ...options }: GasEstimationOptionsPayload) {
|
||||
this.fetcher = fetcher
|
||||
const chainId = options?.chainId || this.configuration.chainId
|
||||
this.configuration.blocksCount = NETWORKS[chainId]?.blocksCount || FEE_HISTORY_BLOCKS
|
||||
this.configuration.percentile = NETWORKS[chainId]?.percentile || FEE_HISTORY_PERCENTILE
|
||||
|
||||
if (options) {
|
||||
this.configuration = { ...this.configuration, ...options }
|
||||
}
|
||||
|
||||
this.cache = new NodeJSCache({ stdTTL: this.configuration.blockTime, useClones: false })
|
||||
}
|
||||
|
||||
public async estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice> {
|
||||
try {
|
||||
const cacheKey = this.FEES_KEY(this.configuration.chainId)
|
||||
const cachedFees = await this.cache.get(cacheKey)
|
||||
|
||||
if (cachedFees) {
|
||||
return cachedFees
|
||||
}
|
||||
|
||||
const { data: latestBlock } = await this.fetcher.makeRpcCall<{ result: Block }>({
|
||||
method: 'eth_getBlockByNumber',
|
||||
params: ['latest', false],
|
||||
})
|
||||
|
||||
if (!latestBlock.result.baseFeePerGas) {
|
||||
throw new Error('An error occurred while fetching current base fee, falling back')
|
||||
}
|
||||
|
||||
const baseFee = fromWeiToGwei(latestBlock.result.baseFeePerGas)
|
||||
|
||||
const blockCount = fromNumberToHex(this.configuration.blocksCount)
|
||||
const rewardPercentiles: number[] = [this.configuration.percentile]
|
||||
|
||||
const { data } = await this.fetcher.makeRpcCall<{ result: FeeHistory }>({
|
||||
method: 'eth_feeHistory',
|
||||
params: [blockCount, 'latest', rewardPercentiles],
|
||||
})
|
||||
|
||||
const fees = await this.calculateFees({ baseFee, feeHistory: data.result })
|
||||
if (this.configuration.shouldCache) {
|
||||
await this.cache.set(cacheKey, fees)
|
||||
}
|
||||
|
||||
return fees
|
||||
} catch (err) {
|
||||
if (fallbackGasPrices) {
|
||||
return fallbackGasPrices
|
||||
}
|
||||
if (this.configuration.fallbackGasPrices) {
|
||||
return this.configuration.fallbackGasPrices
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
private calculatePriorityFeeEstimate(feeHistory?: FeeHistory) {
|
||||
if (!feeHistory) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rewards = feeHistory.reward
|
||||
?.map((r) => fromWeiToGwei(r[0]))
|
||||
.filter((r) => r.isGreaterThan(0))
|
||||
.sort()
|
||||
|
||||
if (!rewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate percentage increases from between ordered list of fees
|
||||
const percentageIncreases = rewards.reduce<BigNumber[]>((acc, curr, i, arr) => {
|
||||
if (i !== arr.length - 1) {
|
||||
const next = arr[i + 1]
|
||||
const percentageIncrease = next.minus(curr).dividedBy(curr).multipliedBy(PERCENT_MULTIPLIER)
|
||||
acc.push(percentageIncrease)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const { highest, index } = findMax(percentageIncreases)
|
||||
// If we have big increased in value, we could be considering "outliers" in our estimate
|
||||
// Skip the low elements and take a new median
|
||||
const values =
|
||||
highest.isGreaterThanOrEqualTo(PRIORITY_FEE_INCREASE_BOUNDARY) && index >= getMedian(rewards)
|
||||
? rewards.slice(index)
|
||||
: rewards
|
||||
|
||||
return values[getMedian(values)]
|
||||
}
|
||||
|
||||
private async getPriorityFromChain(feeHistory?: FeeHistory) {
|
||||
try {
|
||||
const { data } = await this.fetcher.makeRpcCall<{ result: string }>({
|
||||
method: 'eth_maxPriorityFeePerGas',
|
||||
params: [],
|
||||
})
|
||||
|
||||
return fromWeiToGwei(data.result)
|
||||
} catch (err) {
|
||||
return this.calculatePriorityFeeEstimate(feeHistory)
|
||||
}
|
||||
}
|
||||
|
||||
private async calculateFees({ baseFee, feeHistory }: CalculateFeesParams): Promise<EstimatedGasPrice> {
|
||||
const estimatedPriorityFee = await this.getPriorityFromChain(feeHistory)
|
||||
|
||||
const { highest: maxPriorityFeePerGas } = findMax([
|
||||
estimatedPriorityFee ?? BG_ZERO,
|
||||
new BigNumber(this.configuration.minPriority),
|
||||
])
|
||||
const maxFeePerGas = baseFee.plus(maxPriorityFeePerGas)
|
||||
|
||||
if (this.checkIsGreaterThanMax(maxFeePerGas) || this.checkIsGreaterThanMax(maxPriorityFeePerGas)) {
|
||||
throw new Error('Estimated gas fee was much higher than expected, erroring')
|
||||
}
|
||||
|
||||
return {
|
||||
baseFee: baseFee.toNumber(),
|
||||
maxFeePerGas: maxFeePerGas.toNumber(),
|
||||
maxPriorityFeePerGas: maxPriorityFeePerGas.toNumber(),
|
||||
}
|
||||
}
|
||||
|
||||
private checkIsGreaterThanMax(value: BigNumber): boolean {
|
||||
return value.isGreaterThanOrEqualTo(NETWORKS[this.configuration.chainId]?.maxGasPrice) || false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './eip1559'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,39 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import { FeeHistory } from '@/types'
|
||||
import { RpcFetcher } from '@/services'
|
||||
|
||||
export type EstimatedGasPrice = {
|
||||
maxFeePerGas: number
|
||||
baseFee: number | undefined
|
||||
maxPriorityFeePerGas: number
|
||||
}
|
||||
|
||||
export type EstimateFeesParams = {
|
||||
blocksCount: number
|
||||
percentile: number
|
||||
}
|
||||
|
||||
export type CalculateFeesParams = {
|
||||
baseFee: BigNumber
|
||||
feeHistory?: FeeHistory
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
chainId?: number
|
||||
blocksCount?: number
|
||||
percentile?: number
|
||||
blockTime?: number
|
||||
shouldCache?: boolean
|
||||
fallbackGasPrices: EstimatedGasPrice | undefined
|
||||
}
|
||||
|
||||
export type GasEstimationOptionsPayload = Options & {
|
||||
fetcher: RpcFetcher
|
||||
}
|
||||
|
||||
export type Config = Required<Options> & { fallbackGasPrices?: EstimatedGasPrice; minPriority: number }
|
||||
export abstract class EstimateOracle {
|
||||
public configuration: Config
|
||||
public abstract estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice>
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
GasOracleOptions,
|
||||
GasPricesWithEstimateInput,
|
||||
GasPriceWithEstimate,
|
||||
GetGasPriceInput,
|
||||
GetTxGasParamsInput,
|
||||
GetTxGasParamsRes,
|
||||
OracleProvider,
|
||||
} from './types'
|
||||
|
||||
import { ChainId, NETWORKS } from '@/config'
|
||||
import { DEFAULT_TIMEOUT } from '@/constants'
|
||||
import { bumpOnPercent, fromGweiToWeiHex } from '@/utils'
|
||||
|
||||
import {
|
||||
RpcFetcher,
|
||||
GasPrice,
|
||||
LegacyOracle,
|
||||
EstimateOracle,
|
||||
EstimatedGasPrice,
|
||||
LegacyGasPriceOracle,
|
||||
Eip1559GasPriceOracle,
|
||||
} from '@/services'
|
||||
|
||||
export class GasPriceOracle implements OracleProvider {
|
||||
public eip1559: EstimateOracle
|
||||
public legacy: LegacyOracle
|
||||
public fetcher: RpcFetcher
|
||||
private chainId: ChainId
|
||||
public constructor(options?: GasOracleOptions) {
|
||||
const timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
||||
this.chainId = options?.chainId || ChainId.MAINNET
|
||||
const defaultRpc = options?.defaultRpc || NETWORKS[this.chainId].rpcUrl
|
||||
|
||||
this.fetcher = new RpcFetcher(defaultRpc, timeout)
|
||||
|
||||
const { gasPrices, estimated } = options?.fallbackGasPrices || {}
|
||||
|
||||
const payload = { ...options, fetcher: this.fetcher }
|
||||
this.legacy = new LegacyGasPriceOracle({
|
||||
...payload,
|
||||
fallbackGasPrices: gasPrices,
|
||||
})
|
||||
|
||||
this.eip1559 = new Eip1559GasPriceOracle({
|
||||
...payload,
|
||||
fallbackGasPrices: estimated,
|
||||
})
|
||||
}
|
||||
|
||||
public async gasPrices(payload: GetGasPriceInput = {}): Promise<GasPrice | EstimatedGasPrice> {
|
||||
const { fallbackGasPrices, shouldGetMedian, isLegacy = false } = payload
|
||||
if (isLegacy) {
|
||||
return await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||
}
|
||||
try {
|
||||
return await this.eip1559.estimateFees(fallbackGasPrices?.estimated)
|
||||
} catch {
|
||||
return await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||
}
|
||||
}
|
||||
|
||||
public async getTxGasParams(payload: GetTxGasParamsInput = {}): Promise<GetTxGasParamsRes> {
|
||||
const { fallbackGasPrices, shouldGetMedian, isLegacy = false, bumpPercent = 0, legacySpeed = 'fast' } = payload
|
||||
|
||||
if (isLegacy) {
|
||||
const legacyGasPrice = await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||
|
||||
return { gasPrice: fromGweiToWeiHex(bumpOnPercent(legacyGasPrice[legacySpeed], bumpPercent)) }
|
||||
}
|
||||
|
||||
try {
|
||||
const eipParams = await this.eip1559.estimateFees(fallbackGasPrices?.estimated)
|
||||
return {
|
||||
maxFeePerGas: fromGweiToWeiHex(bumpOnPercent(eipParams.maxFeePerGas, bumpPercent)),
|
||||
maxPriorityFeePerGas: fromGweiToWeiHex(bumpOnPercent(eipParams.maxPriorityFeePerGas, bumpPercent)),
|
||||
}
|
||||
} catch {
|
||||
const legacyGasPrice = await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||
|
||||
return { gasPrice: fromGweiToWeiHex(bumpOnPercent(legacyGasPrice[legacySpeed], bumpPercent)) }
|
||||
}
|
||||
}
|
||||
|
||||
public async gasPricesWithEstimate(payload: GasPricesWithEstimateInput = {}): Promise<GasPriceWithEstimate> {
|
||||
const { fallbackGasPrices, shouldGetMedian } = payload
|
||||
|
||||
const estimate = await this.eip1559.estimateFees(fallbackGasPrices?.estimated)
|
||||
const gasPrices = await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||
|
||||
return {
|
||||
estimate,
|
||||
gasPrices,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './gas-price-oracle'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { RpcFetcher, GasPrice, LegacyOracle, EstimatedGasPrice, EstimateOracle, GasPriceKey } from '@/services'
|
||||
|
||||
export type GetTxGasParamsInput = GetGasPriceInput & {
|
||||
bumpPercent?: number
|
||||
legacySpeed?: GasPriceKey
|
||||
}
|
||||
|
||||
export type GetTxGasParamsRes =
|
||||
| {
|
||||
gasPrice: string
|
||||
}
|
||||
| {
|
||||
maxFeePerGas: string
|
||||
maxPriorityFeePerGas: string
|
||||
}
|
||||
|
||||
export type GetGasPriceInput = {
|
||||
isLegacy?: boolean
|
||||
shouldGetMedian?: boolean
|
||||
fallbackGasPrices?: FallbackGasPrices
|
||||
}
|
||||
|
||||
export type FallbackGasPrices = {
|
||||
gasPrices?: GasPrice
|
||||
estimated?: EstimatedGasPrice
|
||||
}
|
||||
|
||||
export type GasPricesWithEstimateInput = {
|
||||
shouldGetMedian?: boolean
|
||||
fallbackGasPrices?: FallbackGasPrices
|
||||
}
|
||||
|
||||
export type GasOracleOptions = {
|
||||
chainId?: number
|
||||
timeout?: number
|
||||
defaultRpc?: string
|
||||
blocksCount?: number
|
||||
percentile?: number
|
||||
blockTime?: number
|
||||
shouldCache?: boolean
|
||||
minPriority?: number
|
||||
fallbackGasPrices?: FallbackGasPrices
|
||||
}
|
||||
|
||||
export type GasPriceWithEstimate = {
|
||||
gasPrices: GasPrice
|
||||
estimate: EstimatedGasPrice
|
||||
}
|
||||
|
||||
export interface OracleProvider {
|
||||
eip1559: EstimateOracle
|
||||
legacy: LegacyOracle
|
||||
fetcher: RpcFetcher
|
||||
gasPrices: (payload: GetGasPriceInput) => Promise<GasPrice | EstimatedGasPrice>
|
||||
gasPricesWithEstimate: (payload: GasPricesWithEstimateInput) => Promise<GasPriceWithEstimate>
|
||||
getTxGasParams: (payload: GetTxGasParamsInput) => Promise<GetTxGasParamsRes>
|
||||
}
|
||||
7
@tornado/gas-price-oracle/src/services/index.ts
Normal file
7
@tornado/gas-price-oracle/src/services/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './gas-price-oracle'
|
||||
|
||||
export * from './gas-estimation'
|
||||
export * from './legacy-gas-price'
|
||||
|
||||
export * from './cacher'
|
||||
export * from './rpcFetcher'
|
||||
@@ -0,0 +1,10 @@
|
||||
const DEFAULT_GAS_PRICE = { instant: 0, fast: 0, standard: 0, low: 0 }
|
||||
|
||||
const MULTIPLIERS = {
|
||||
instant: 1.3,
|
||||
fast: 1.2,
|
||||
standard: 1.1,
|
||||
low: 1,
|
||||
}
|
||||
|
||||
export { MULTIPLIERS, DEFAULT_GAS_PRICE }
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './legacy'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,318 @@
|
||||
import axios from 'axios'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import {
|
||||
GasPrice,
|
||||
GasPriceKey,
|
||||
LegacyOracle,
|
||||
OnChainOracle,
|
||||
OffChainOracle,
|
||||
LegacyOptions,
|
||||
OnChainOracles,
|
||||
OffChainOracles,
|
||||
LegacyOptionsPayload,
|
||||
GetGasPriceFromRespInput,
|
||||
} from './types'
|
||||
|
||||
import { ChainId, NETWORKS } from '@/config'
|
||||
import { RpcFetcher, NodeJSCache } from '@/services'
|
||||
import { resolvePropertyPath } from '@/utils'
|
||||
import { GWEI, DEFAULT_TIMEOUT, GWEI_PRECISION, DEFAULT_BLOCK_DURATION } from '@/constants'
|
||||
|
||||
import { MULTIPLIERS, DEFAULT_GAS_PRICE } from './constants'
|
||||
|
||||
export class LegacyGasPriceOracle implements LegacyOracle {
|
||||
static getMedianGasPrice(gasPrices: GasPrice[]): GasPrice {
|
||||
const medianGasPrice: GasPrice = DEFAULT_GAS_PRICE
|
||||
|
||||
const results: Record<GasPriceKey, number[]> = {
|
||||
instant: [],
|
||||
fast: [],
|
||||
standard: [],
|
||||
low: [],
|
||||
}
|
||||
|
||||
for (const gasPrice of gasPrices) {
|
||||
results.instant.push(gasPrice.instant)
|
||||
results.fast.push(gasPrice.fast)
|
||||
results.standard.push(gasPrice.standard)
|
||||
results.low.push(gasPrice.low)
|
||||
}
|
||||
|
||||
for (const type of Object.keys(medianGasPrice) as (keyof GasPrice)[]) {
|
||||
const allPrices = results[type].sort((a, b) => a - b)
|
||||
if (allPrices.length === 1) {
|
||||
medianGasPrice[type] = allPrices[0]
|
||||
continue
|
||||
} else if (allPrices.length === 0) {
|
||||
continue
|
||||
}
|
||||
const isEven = allPrices.length % 2 === 0
|
||||
const middle = Math.floor(allPrices.length / 2)
|
||||
medianGasPrice[type] = isEven ? (allPrices[middle - 1] + allPrices[middle]) / 2.0 : allPrices[middle]
|
||||
}
|
||||
|
||||
return LegacyGasPriceOracle.normalize(medianGasPrice)
|
||||
}
|
||||
|
||||
static getMultipliedPrices(gasPrice: number): GasPrice {
|
||||
return {
|
||||
instant: gasPrice * MULTIPLIERS.instant,
|
||||
fast: gasPrice * MULTIPLIERS.fast,
|
||||
standard: gasPrice * MULTIPLIERS.standard,
|
||||
low: gasPrice * MULTIPLIERS.low,
|
||||
}
|
||||
}
|
||||
|
||||
static normalize(_gas: GasPrice): GasPrice {
|
||||
const format = {
|
||||
groupSeparator: '',
|
||||
decimalSeparator: '.',
|
||||
}
|
||||
|
||||
const gas: GasPrice = { ..._gas }
|
||||
for (const type of Object.keys(gas) as (keyof GasPrice)[]) {
|
||||
gas[type] = Number(new BigNumber(gas[type]).toFormat(GWEI_PRECISION, format))
|
||||
}
|
||||
|
||||
return gas
|
||||
}
|
||||
|
||||
static getCategorize(gasPrice: number): GasPrice {
|
||||
return LegacyGasPriceOracle.normalize(LegacyGasPriceOracle.getMultipliedPrices(gasPrice))
|
||||
}
|
||||
|
||||
static getGasPriceFromResponse(payload: GetGasPriceFromRespInput): number {
|
||||
const { response, fetcherName, denominator = GWEI } = payload
|
||||
let fastGasPrice = new BigNumber(response)
|
||||
if (fastGasPrice.isZero()) {
|
||||
throw new Error(`${fetcherName} provides corrupted values`)
|
||||
}
|
||||
fastGasPrice = fastGasPrice.div(denominator)
|
||||
return fastGasPrice.toNumber()
|
||||
}
|
||||
|
||||
public lastGasPrice: GasPrice
|
||||
public onChainOracles: OnChainOracles = {}
|
||||
public offChainOracles: OffChainOracles = {}
|
||||
public configuration: Required<LegacyOptions> = {
|
||||
shouldCache: false,
|
||||
chainId: ChainId.MAINNET,
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
blockTime: DEFAULT_BLOCK_DURATION,
|
||||
defaultRpc: NETWORKS[ChainId.MAINNET].rpcUrl,
|
||||
fallbackGasPrices: LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice),
|
||||
}
|
||||
|
||||
private readonly fetcher: RpcFetcher
|
||||
|
||||
private cache: NodeJSCache<GasPrice>
|
||||
private LEGACY_KEY = (chainId: ChainId) => `legacy-fee-${chainId}`
|
||||
|
||||
constructor({ fetcher, ...options }: LegacyOptionsPayload) {
|
||||
this.fetcher = fetcher
|
||||
if (options) {
|
||||
this.configuration = { ...this.configuration, ...options }
|
||||
}
|
||||
|
||||
const { defaultGasPrice } = NETWORKS[ChainId.MAINNET]
|
||||
const fallbackGasPrices = this.configuration.fallbackGasPrices || LegacyGasPriceOracle.getMultipliedPrices(defaultGasPrice)
|
||||
this.configuration.fallbackGasPrices = LegacyGasPriceOracle.normalize(fallbackGasPrices)
|
||||
|
||||
const network = NETWORKS[this.configuration.chainId]?.oracles
|
||||
if (network) {
|
||||
this.offChainOracles = { ...network.offChainOracles }
|
||||
this.onChainOracles = { ...network.onChainOracles }
|
||||
}
|
||||
|
||||
this.cache = new NodeJSCache({ stdTTL: this.configuration.blockTime, useClones: false })
|
||||
}
|
||||
|
||||
public addOffChainOracle(oracle: OffChainOracle): void {
|
||||
this.offChainOracles[oracle.name] = oracle
|
||||
}
|
||||
|
||||
public addOnChainOracle(oracle: OnChainOracle): void {
|
||||
this.onChainOracles[oracle.name] = oracle
|
||||
}
|
||||
|
||||
public removeOnChainOracle(name: string): void {
|
||||
delete this.onChainOracles[name]
|
||||
}
|
||||
|
||||
public removeOffChainOracle(name: string): void {
|
||||
delete this.offChainOracles[name]
|
||||
}
|
||||
|
||||
public async fetchGasPricesOnChain(): Promise<number> {
|
||||
for (const oracle of Object.values(this.onChainOracles)) {
|
||||
const { name, callData, contract, denominator, rpc } = oracle
|
||||
|
||||
try {
|
||||
const response = await this.fetcher.makeRpcCall<{ result: string | number }>({
|
||||
rpc,
|
||||
method: 'eth_call',
|
||||
params: [{ data: callData, to: contract }, 'latest'],
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
return LegacyGasPriceOracle.getGasPriceFromResponse({
|
||||
denominator,
|
||||
fetcherName: `${name} oracle`,
|
||||
response: response.data.result,
|
||||
})
|
||||
}
|
||||
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
throw new Error('All oracles are down. Probably a network error.')
|
||||
}
|
||||
|
||||
public async fetchGasPriceFromRpc(): Promise<number> {
|
||||
try {
|
||||
const { status, data } = await this.fetcher.makeRpcCall<{ result: string | number }>({
|
||||
params: [],
|
||||
method: 'eth_gasPrice',
|
||||
})
|
||||
|
||||
if (status === 200) {
|
||||
return LegacyGasPriceOracle.getGasPriceFromResponse({
|
||||
fetcherName: 'Default RPC',
|
||||
response: data.result,
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error(`Fetch gasPrice from default RPC failed..`)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
throw new Error('Default RPC is down. Probably a network error.')
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchGasPricesOffChain(shouldGetMedian = true): Promise<GasPrice> {
|
||||
if (shouldGetMedian) {
|
||||
return await this.fetchMedianGasPriceOffChain()
|
||||
}
|
||||
|
||||
for (const oracle of Object.values(this.offChainOracles)) {
|
||||
try {
|
||||
return await this.askOracle(oracle)
|
||||
} catch (e) {
|
||||
console.info(`${oracle} has error - `, e.message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
throw new Error('All oracles are down. Probably a network error.')
|
||||
}
|
||||
|
||||
public async fetchMedianGasPriceOffChain(): Promise<GasPrice> {
|
||||
const promises: Promise<GasPrice>[] = []
|
||||
|
||||
for (const oracle of Object.values(this.offChainOracles) as OffChainOracle[]) {
|
||||
promises.push(this.askOracle(oracle))
|
||||
}
|
||||
|
||||
const settledPromises = await Promise.allSettled(promises)
|
||||
|
||||
const allGasPrices = settledPromises.reduce((acc: GasPrice[], result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
acc.push(result.value)
|
||||
return acc
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
if (allGasPrices.length === 0) {
|
||||
throw new Error('All oracles are down. Probably a network error.')
|
||||
}
|
||||
|
||||
return LegacyGasPriceOracle.getMedianGasPrice(allGasPrices)
|
||||
}
|
||||
|
||||
public async gasPrices(fallbackGasPrices?: GasPrice, shouldGetMedian = true): Promise<GasPrice> {
|
||||
if (!this.lastGasPrice) {
|
||||
this.lastGasPrice = fallbackGasPrices || this.configuration.fallbackGasPrices
|
||||
}
|
||||
|
||||
const cacheKey = this.LEGACY_KEY(this.configuration.chainId)
|
||||
const cachedFees = await this.cache.get(cacheKey)
|
||||
|
||||
if (cachedFees) {
|
||||
return cachedFees
|
||||
}
|
||||
|
||||
if (Object.keys(this.offChainOracles).length > 0) {
|
||||
try {
|
||||
this.lastGasPrice = await this.fetchGasPricesOffChain(shouldGetMedian)
|
||||
if (this.configuration.shouldCache) {
|
||||
await this.cache.set(cacheKey, this.lastGasPrice)
|
||||
}
|
||||
return this.lastGasPrice
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch gas prices from offchain oracles...')
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(this.onChainOracles).length > 0) {
|
||||
try {
|
||||
const fastGas = await this.fetchGasPricesOnChain()
|
||||
|
||||
this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
|
||||
if (this.configuration.shouldCache) {
|
||||
await this.cache.set(cacheKey, this.lastGasPrice)
|
||||
}
|
||||
return this.lastGasPrice
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch gas prices from onchain oracles...')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const fastGas = await this.fetchGasPriceFromRpc()
|
||||
|
||||
this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
|
||||
if (this.configuration.shouldCache) {
|
||||
await this.cache.set(cacheKey, this.lastGasPrice)
|
||||
}
|
||||
return this.lastGasPrice
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch gas prices from default RPC. Last known gas will be returned')
|
||||
}
|
||||
return LegacyGasPriceOracle.normalize(this.lastGasPrice)
|
||||
}
|
||||
|
||||
public async askOracle(oracle: OffChainOracle): Promise<GasPrice> {
|
||||
const {
|
||||
url,
|
||||
name,
|
||||
denominator,
|
||||
lowPropertyName,
|
||||
fastPropertyName,
|
||||
instantPropertyName,
|
||||
standardPropertyName,
|
||||
additionalDataProperty,
|
||||
} = oracle
|
||||
|
||||
const response = await axios.get(url, { timeout: this.configuration.timeout })
|
||||
|
||||
if (response.status === 200) {
|
||||
const gas = resolvePropertyPath(response.data, additionalDataProperty)
|
||||
|
||||
if (Number(resolvePropertyPath(gas, fastPropertyName)) === 0) {
|
||||
throw new Error(`${name} oracle provides corrupted values`)
|
||||
}
|
||||
|
||||
const gasPrices: GasPrice = {
|
||||
instant: parseFloat(resolvePropertyPath(gas, instantPropertyName)) / denominator,
|
||||
fast: parseFloat(resolvePropertyPath(gas, fastPropertyName)) / denominator,
|
||||
standard: parseFloat(resolvePropertyPath(gas, standardPropertyName)) / denominator,
|
||||
low: parseFloat(resolvePropertyPath(gas, lowPropertyName)) / denominator,
|
||||
}
|
||||
return LegacyGasPriceOracle.normalize(gasPrices)
|
||||
} else {
|
||||
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { RpcFetcher } from '@/services'
|
||||
|
||||
export type OffChainOracle = {
|
||||
url: string
|
||||
name: string
|
||||
denominator: number
|
||||
lowPropertyName: string
|
||||
fastPropertyName: string
|
||||
instantPropertyName: string
|
||||
standardPropertyName: string
|
||||
additionalDataProperty: string | null
|
||||
}
|
||||
|
||||
export type OffChainOracles = Record<string, OffChainOracle>
|
||||
|
||||
export type OnChainOracle = {
|
||||
name: string
|
||||
rpc?: string
|
||||
contract: string
|
||||
callData: string
|
||||
denominator: string
|
||||
}
|
||||
|
||||
export type OnChainOracles = Record<string, OnChainOracle>
|
||||
|
||||
export type AllOracles = {
|
||||
onChainOracles: OnChainOracles
|
||||
offChainOracles: OffChainOracles
|
||||
}
|
||||
|
||||
export type NetworkConfig = Record<number, AllOracles>
|
||||
|
||||
export type GasPriceKey = 'instant' | 'fast' | 'standard' | 'low'
|
||||
export type GasPrice = Record<GasPriceKey, number>
|
||||
|
||||
export type LegacyOptions = {
|
||||
chainId?: number
|
||||
timeout?: number
|
||||
blockTime?: number
|
||||
defaultRpc?: string
|
||||
shouldCache?: boolean
|
||||
fallbackGasPrices?: GasPrice
|
||||
}
|
||||
|
||||
export type LegacyOptionsPayload = LegacyOptions & {
|
||||
fetcher: RpcFetcher
|
||||
}
|
||||
|
||||
export type GetGasPriceFromRespInput = {
|
||||
fetcherName: string
|
||||
response: string | number
|
||||
denominator?: number | string
|
||||
}
|
||||
|
||||
type Config = Required<LegacyOptions>
|
||||
|
||||
export abstract class LegacyOracle {
|
||||
static normalize: (_gas: GasPrice) => GasPrice
|
||||
static getCategorize: (gasPrice: number) => GasPrice
|
||||
static getMultipliedPrices: (gasPrice: number) => GasPrice
|
||||
static getMedianGasPrice: (gasPrices: GasPrice[]) => GasPrice
|
||||
static getGasPriceFromResponse: (payload: GetGasPriceFromRespInput) => Promise<number>
|
||||
|
||||
public configuration: Config
|
||||
public lastGasPrice: GasPrice
|
||||
public onChainOracles: OnChainOracles
|
||||
public offChainOracles: OffChainOracles
|
||||
|
||||
public abstract removeOnChainOracle(name: string): void
|
||||
public abstract removeOffChainOracle(name: string): void
|
||||
public abstract addOnChainOracle(oracle: OnChainOracle): void
|
||||
public abstract addOffChainOracle(oracle: OffChainOracle): void
|
||||
|
||||
public fetchGasPriceFromRpc: () => Promise<number>
|
||||
public fetchGasPricesOnChain: () => Promise<number>
|
||||
public fetchMedianGasPriceOffChain: () => Promise<GasPrice>
|
||||
public askOracle: (oracle: OffChainOracle) => Promise<GasPrice>
|
||||
public fetchGasPricesOffChain: (shouldGetMedian?: boolean) => Promise<GasPrice>
|
||||
public gasPrices: (fallbackGasPrices?: GasPrice, shouldGetMedian?: boolean) => Promise<GasPrice>
|
||||
}
|
||||
23
@tornado/gas-price-oracle/src/services/rpcFetcher/fetcher.ts
Normal file
23
@tornado/gas-price-oracle/src/services/rpcFetcher/fetcher.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import { MakeRpcCallInput } from './types'
|
||||
|
||||
export class RpcFetcher {
|
||||
private readonly rpc: string
|
||||
private readonly timeout: number
|
||||
|
||||
constructor(rpc: string, timeout: number) {
|
||||
this.rpc = rpc
|
||||
this.timeout = timeout
|
||||
}
|
||||
|
||||
public async makeRpcCall<R>({ rpc, timeout, method, params }: MakeRpcCallInput): Promise<AxiosResponse<R>> {
|
||||
const body = {
|
||||
method,
|
||||
id: 1337,
|
||||
jsonrpc: '2.0',
|
||||
params: params,
|
||||
}
|
||||
|
||||
return await axios.post<R>(rpc || this.rpc, body, { timeout: timeout || this.timeout })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './fetcher'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Params = (string | number | number[] | Record<string, string | number | boolean> | boolean)[]
|
||||
export type MakeRpcCallInput = {
|
||||
rpc?: string
|
||||
method: string
|
||||
timeout?: number
|
||||
params: Params
|
||||
}
|
||||
134
@tornado/gas-price-oracle/src/tests/complex.test.ts
Normal file
134
@tornado/gas-price-oracle/src/tests/complex.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import chai from 'chai'
|
||||
import BigNumber from 'bignumber.js'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import mockery from 'mockery'
|
||||
import { before, describe } from 'mocha'
|
||||
|
||||
import { GWEI_PRECISION } from '@/constants'
|
||||
import { GasPriceOracle, GasPrice } from '@/services'
|
||||
|
||||
chai.use(require('chai-as-promised'))
|
||||
chai.should()
|
||||
|
||||
let oracle = new GasPriceOracle()
|
||||
|
||||
before('before', async () => {
|
||||
const gasPrice = await oracle.legacy.gasPrices()
|
||||
console.log('legacy gasPrice - ', { gasPrice })
|
||||
})
|
||||
|
||||
before('before', function () {
|
||||
const axiosMock = {
|
||||
get: () => {
|
||||
throw new Error('axios GET method is mocked for tests')
|
||||
},
|
||||
post: () => {
|
||||
throw new Error('axios POST method is mocked for tests')
|
||||
},
|
||||
}
|
||||
mockery.registerMock('axios', axiosMock)
|
||||
})
|
||||
|
||||
beforeEach('beforeEach', function () {
|
||||
oracle = new GasPriceOracle()
|
||||
})
|
||||
|
||||
const INJECTED_CHAIN_ID = 5
|
||||
const INJECTED_RPC_URL = 'https://rpc.goerli.mudit.blog/'
|
||||
|
||||
describe('complex test', function () {
|
||||
describe('fetching data', function () {
|
||||
describe('should work with unexpected chainId', function () {
|
||||
// it('legacy', async function () {
|
||||
// const newOracle = new GasPriceOracle({ chainId: ChainId.XDAI })
|
||||
//
|
||||
// for (let i = 0; i < 100; i++) {
|
||||
// const res = await newOracle.eip1559.estimateFees()
|
||||
// await new Promise((r) =>
|
||||
// setTimeout(() => {
|
||||
// r(console.log(`res - ${i}`, res))
|
||||
// }, 3000),
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
|
||||
it('legacy', async function () {
|
||||
const newOracle = new GasPriceOracle({ timeout: 1337, chainId: INJECTED_CHAIN_ID, defaultRpc: INJECTED_RPC_URL })
|
||||
|
||||
const goerliGas = (await newOracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
|
||||
if ('instant' in goerliGas) {
|
||||
goerliGas.instant.should.be.a('number')
|
||||
goerliGas.fast.should.be.a('number')
|
||||
goerliGas.standard.should.be.a('number')
|
||||
goerliGas.low.should.be.a('number')
|
||||
}
|
||||
|
||||
newOracle.legacy.configuration.defaultRpc.should.be.equal(INJECTED_RPC_URL)
|
||||
newOracle.legacy.configuration.chainId.should.be.equal(INJECTED_CHAIN_ID)
|
||||
})
|
||||
it('eip-1559', async function () {
|
||||
const newOracle = new GasPriceOracle({ timeout: 1337, chainId: INJECTED_CHAIN_ID, defaultRpc: INJECTED_RPC_URL })
|
||||
|
||||
const goerliEstimated = await newOracle.eip1559.estimateFees()
|
||||
if (goerliEstimated.baseFee) {
|
||||
goerliEstimated.baseFee.should.be.a('number')
|
||||
|
||||
goerliEstimated.maxFeePerGas.should.be.at.least(goerliEstimated.baseFee)
|
||||
const estimatedMaxFee = new BigNumber(goerliEstimated.baseFee)
|
||||
.plus(goerliEstimated.maxPriorityFeePerGas)
|
||||
.decimalPlaces(GWEI_PRECISION)
|
||||
.toNumber()
|
||||
goerliEstimated.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||
}
|
||||
|
||||
goerliEstimated.maxFeePerGas.should.be.a('number')
|
||||
goerliEstimated.maxPriorityFeePerGas.should.be.a('number')
|
||||
|
||||
newOracle.legacy.configuration.defaultRpc.should.be.equal(INJECTED_RPC_URL)
|
||||
newOracle.legacy.configuration.chainId.should.be.equal(INJECTED_CHAIN_ID)
|
||||
})
|
||||
it('without selected strategy', async function () {
|
||||
const newOracle = new GasPriceOracle({ timeout: 1337, chainId: INJECTED_CHAIN_ID, defaultRpc: INJECTED_RPC_URL })
|
||||
|
||||
const gasPrice = await newOracle.gasPrices({})
|
||||
|
||||
if ('baseFee' in gasPrice && gasPrice.baseFee) {
|
||||
gasPrice.baseFee.should.be.a('number')
|
||||
|
||||
gasPrice.maxFeePerGas.should.be.at.least(gasPrice.baseFee)
|
||||
const estimatedMaxFee = new BigNumber(gasPrice.baseFee)
|
||||
.plus(gasPrice.maxPriorityFeePerGas)
|
||||
.decimalPlaces(GWEI_PRECISION)
|
||||
.toNumber()
|
||||
gasPrice.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||
}
|
||||
|
||||
if ('maxFeePerGas' in gasPrice) {
|
||||
gasPrice.maxFeePerGas.should.be.a('number')
|
||||
gasPrice.maxPriorityFeePerGas.should.be.a('number')
|
||||
}
|
||||
|
||||
if ('instant' in gasPrice) {
|
||||
gasPrice.instant.should.be.a('number')
|
||||
gasPrice.fast.should.be.a('number')
|
||||
gasPrice.standard.should.be.a('number')
|
||||
gasPrice.low.should.be.a('number')
|
||||
}
|
||||
|
||||
newOracle.legacy.configuration.defaultRpc.should.be.equal(INJECTED_RPC_URL)
|
||||
newOracle.legacy.configuration.chainId.should.be.equal(INJECTED_CHAIN_ID)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after('after', function () {
|
||||
after(function () {
|
||||
mockery.disable()
|
||||
mockery.deregisterMock('node-fetch')
|
||||
})
|
||||
})
|
||||
163
@tornado/gas-price-oracle/src/tests/eip1559.test.ts
Normal file
163
@tornado/gas-price-oracle/src/tests/eip1559.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import chai from 'chai'
|
||||
import BigNumber from 'bignumber.js'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import mockery from 'mockery'
|
||||
import { before, describe } from 'mocha'
|
||||
|
||||
import { sleep } from '@/utils'
|
||||
import { ChainId, NETWORKS } from '@/config'
|
||||
import { GWEI_PRECISION } from '@/constants'
|
||||
|
||||
import { GasPriceOracle } from '@/services/gas-price-oracle'
|
||||
import { EstimatedGasPrice } from '@/services/gas-estimation'
|
||||
import { FALLBACK_ESTIMATE } from '@/services/gas-estimation/constants'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
chai.should()
|
||||
|
||||
let oracle = new GasPriceOracle()
|
||||
|
||||
before('before', async () => {
|
||||
const gasPrice = await oracle.eip1559.estimateFees()
|
||||
console.log('eip estimation -', { gasPrice })
|
||||
})
|
||||
|
||||
//
|
||||
beforeEach('beforeEach', function () {
|
||||
oracle = new GasPriceOracle()
|
||||
})
|
||||
|
||||
const INJECTED_RPC_URL = 'https://cloudflare-eth.com'
|
||||
describe('eip-1559 gasOracle', function () {
|
||||
describe('eip constructor', function () {
|
||||
it('should set default values', function () {
|
||||
oracle.eip1559.configuration.blocksCount.should.be.equal(NETWORKS[oracle.eip1559.configuration.chainId].blocksCount)
|
||||
oracle.eip1559.configuration.percentile.should.be.equal(NETWORKS[oracle.eip1559.configuration.chainId].percentile)
|
||||
})
|
||||
|
||||
it('should set passed values', function () {
|
||||
const newHistoryBlocks = 15
|
||||
const newHistoryPercentile = 10
|
||||
|
||||
const newOracle = new GasPriceOracle({
|
||||
blocksCount: newHistoryBlocks,
|
||||
percentile: newHistoryPercentile,
|
||||
})
|
||||
|
||||
newOracle.eip1559.configuration.blocksCount.should.be.equal(newHistoryBlocks)
|
||||
newOracle.eip1559.configuration.percentile.should.be.equal(newHistoryPercentile)
|
||||
})
|
||||
})
|
||||
|
||||
const chains = Object.keys(NETWORKS).map((id) => Number(id))
|
||||
|
||||
chains.forEach((chainId) => {
|
||||
let eipOracle = new GasPriceOracle({ chainId })
|
||||
|
||||
describe(`estimateGas ${chainId}`, function () {
|
||||
it('should return error if not eip-1559 not supported', async function () {
|
||||
if (chainId === ChainId.OPTIMISM || chainId === ChainId.BSC) {
|
||||
await eipOracle.eip1559
|
||||
.estimateFees()
|
||||
.should.be.rejectedWith('An error occurred while fetching current base fee, falling back')
|
||||
}
|
||||
})
|
||||
|
||||
if (chainId === ChainId.OPTIMISM || chainId === ChainId.BSC) {
|
||||
return
|
||||
}
|
||||
|
||||
it('should work', async function () {
|
||||
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
|
||||
|
||||
console.log(`estimateGas ${chainId}`, estimateGas)
|
||||
if (estimateGas.baseFee) {
|
||||
estimateGas.baseFee.should.be.a('number')
|
||||
estimateGas.maxFeePerGas.should.be.a('number')
|
||||
estimateGas.maxPriorityFeePerGas.should.be.a('number')
|
||||
|
||||
estimateGas.maxFeePerGas.should.be.at.least(estimateGas.baseFee)
|
||||
const estimatedMaxFee = new BigNumber(estimateGas.baseFee)
|
||||
.plus(estimateGas.maxPriorityFeePerGas)
|
||||
.decimalPlaces(GWEI_PRECISION)
|
||||
.toNumber()
|
||||
estimateGas.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||
}
|
||||
})
|
||||
|
||||
it('should work with crashed rpc (return default data)', async function () {
|
||||
eipOracle = new GasPriceOracle({ defaultRpc: 'wrongRpcUrl', chainId })
|
||||
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees(FALLBACK_ESTIMATE)
|
||||
|
||||
if (estimateGas.baseFee) {
|
||||
estimateGas.baseFee.should.be.a('number')
|
||||
estimateGas.maxFeePerGas.should.be.a('number')
|
||||
estimateGas.maxPriorityFeePerGas.should.be.a('number')
|
||||
|
||||
estimateGas.baseFee.should.be.at.equal(FALLBACK_ESTIMATE.baseFee)
|
||||
estimateGas.maxFeePerGas.should.be.at.equal(FALLBACK_ESTIMATE.maxFeePerGas)
|
||||
estimateGas.maxPriorityFeePerGas.should.be.at.equal(FALLBACK_ESTIMATE.maxPriorityFeePerGas)
|
||||
}
|
||||
})
|
||||
|
||||
it('should work with custom rpc', async function () {
|
||||
eipOracle = new GasPriceOracle({ defaultRpc: INJECTED_RPC_URL, chainId })
|
||||
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees(FALLBACK_ESTIMATE)
|
||||
|
||||
if (estimateGas.baseFee) {
|
||||
estimateGas.baseFee.should.be.a('number')
|
||||
estimateGas.maxFeePerGas.should.be.a('number')
|
||||
estimateGas.maxPriorityFeePerGas.should.be.a('number')
|
||||
|
||||
const estimatedMaxFee = new BigNumber(estimateGas.baseFee)
|
||||
.plus(estimateGas.maxPriorityFeePerGas)
|
||||
.decimalPlaces(GWEI_PRECISION)
|
||||
.toNumber()
|
||||
estimateGas.maxFeePerGas.should.be.at.least(estimateGas.baseFee)
|
||||
estimateGas.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||
}
|
||||
})
|
||||
it('should cache', async function () {
|
||||
eipOracle = new GasPriceOracle({ shouldCache: true, chainId })
|
||||
const estimateGasFirst: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
|
||||
|
||||
await sleep(2000)
|
||||
const estimateGasSecond: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
|
||||
|
||||
if (estimateGasFirst?.maxFeePerGas) {
|
||||
estimateGasFirst.maxFeePerGas.should.be.at.equal(estimateGasSecond?.maxFeePerGas)
|
||||
}
|
||||
|
||||
await sleep(4000)
|
||||
const estimateGasThird: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
|
||||
|
||||
if (estimateGasSecond?.maxFeePerGas) {
|
||||
estimateGasSecond.maxFeePerGas.should.be.at.equal(estimateGasThird?.maxFeePerGas)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('estimate ARBITRUM', function () {
|
||||
it('should be priority 0', async function () {
|
||||
const eipOracle = new GasPriceOracle({ minPriority: 0, chainId: ChainId.ARBITRUM })
|
||||
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees(FALLBACK_ESTIMATE)
|
||||
|
||||
console.log('estimateGas.maxPriorityFeePerGas', estimateGas.maxPriorityFeePerGas)
|
||||
estimateGas.maxPriorityFeePerGas.should.be.at.equal(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after('after', function () {
|
||||
after(function () {
|
||||
mockery.disable()
|
||||
mockery.deregisterMock('node-fetch')
|
||||
})
|
||||
})
|
||||
383
@tornado/gas-price-oracle/src/tests/legacy.test.ts
Normal file
383
@tornado/gas-price-oracle/src/tests/legacy.test.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import chai from 'chai'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import mockery from 'mockery'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { before, describe } from 'mocha'
|
||||
|
||||
import { sleep } from '@/utils'
|
||||
import { ChainId, NETWORKS } from '@/config'
|
||||
import { DEFAULT_TIMEOUT } from '@/constants'
|
||||
import { GasPriceOracle } from '@/services/gas-price-oracle'
|
||||
import { GasPrice, LegacyGasPriceOracle, OffChainOracle } from '@/services/legacy-gas-price'
|
||||
|
||||
chai.use(require('chai-as-promised'))
|
||||
chai.should()
|
||||
|
||||
let oracle = new GasPriceOracle()
|
||||
let { onChainOracles, offChainOracles } = oracle.legacy
|
||||
|
||||
before('before', async () => {
|
||||
const gasPrice = await oracle.legacy.gasPrices()
|
||||
console.log('legacy gasPrice - ', { gasPrice })
|
||||
})
|
||||
|
||||
before('before', function () {
|
||||
const axiosMock = {
|
||||
get: () => {
|
||||
throw new Error('axios GET method is mocked for tests')
|
||||
},
|
||||
post: () => {
|
||||
throw new Error('axios POST method is mocked for tests')
|
||||
},
|
||||
}
|
||||
mockery.registerMock('axios', axiosMock)
|
||||
})
|
||||
|
||||
beforeEach('beforeEach', function () {
|
||||
oracle = new GasPriceOracle()
|
||||
;({ onChainOracles, offChainOracles } = oracle.legacy)
|
||||
})
|
||||
|
||||
const INJECTED_RPC_URL = 'https://cloudflare-eth.com'
|
||||
|
||||
describe('legacy gasOracle', function () {
|
||||
describe('legacy constructor', function () {
|
||||
it('should set default values', function () {
|
||||
oracle.legacy.configuration.defaultRpc.should.be.equal(NETWORKS[ChainId.MAINNET].rpcUrl)
|
||||
oracle.legacy.configuration.timeout.should.be.equal(DEFAULT_TIMEOUT)
|
||||
})
|
||||
|
||||
it('should set passed values', function () {
|
||||
const newOracle = new GasPriceOracle({ timeout: 1337 })
|
||||
|
||||
newOracle.legacy.configuration.defaultRpc.should.be.equal(NETWORKS[ChainId.MAINNET].rpcUrl)
|
||||
newOracle.legacy.configuration.timeout.should.be.equal(1337)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchGasPricesOffChain', function () {
|
||||
it('should work', async function () {
|
||||
const gas: GasPrice = await oracle.legacy.fetchGasPricesOffChain(true)
|
||||
|
||||
gas.instant.should.be.a('number')
|
||||
gas.fast.should.be.a('number')
|
||||
gas.standard.should.be.a('number')
|
||||
gas.low.should.be.a('number')
|
||||
|
||||
gas.instant.should.be.at.least(gas.fast)
|
||||
gas.fast.should.be.at.least(gas.standard)
|
||||
gas.standard.should.be.at.least(gas.low)
|
||||
gas.low.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should throw if all offchain oracles are down', async function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
const { GasPriceOracle } = require('../index')
|
||||
oracle = new GasPriceOracle()
|
||||
// @ts-ignore
|
||||
await oracle.legacy.fetchGasPricesOffChain(true).should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||
mockery.disable()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchGasPricesOnChain', function () {
|
||||
it('should work', async function () {
|
||||
const gas: number = await oracle.legacy.fetchGasPricesOnChain()
|
||||
gas.should.be.a('number')
|
||||
gas.should.be.above(1)
|
||||
gas.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should work with custom rpc', async function () {
|
||||
const rpc = INJECTED_RPC_URL
|
||||
oracle = new GasPriceOracle({ defaultRpc: rpc })
|
||||
oracle.legacy.configuration.defaultRpc.should.be.equal(rpc)
|
||||
const gas: number = await oracle.legacy.fetchGasPricesOnChain()
|
||||
|
||||
gas.should.be.a('number')
|
||||
|
||||
gas.should.be.above(1)
|
||||
gas.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should remove oracle', async function () {
|
||||
await oracle.legacy.fetchGasPricesOnChain()
|
||||
oracle.legacy.removeOnChainOracle('chainlink')
|
||||
// @ts-ignore
|
||||
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||
})
|
||||
|
||||
it('should add oracle', async function () {
|
||||
const toAdd = onChainOracles.chainlink
|
||||
await oracle.legacy.fetchGasPricesOnChain()
|
||||
oracle.legacy.removeOnChainOracle('chainlink')
|
||||
|
||||
// @ts-ignore
|
||||
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||
|
||||
oracle.legacy.addOnChainOracle(toAdd)
|
||||
const gas: number = await oracle.legacy.fetchGasPricesOnChain()
|
||||
|
||||
gas.should.be.a('number')
|
||||
gas.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should throw if all onchain oracles are down', async function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
const { GasPriceOracle } = require('../index')
|
||||
|
||||
oracle = new GasPriceOracle()
|
||||
// @ts-ignore
|
||||
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||
mockery.disable()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchGasPriceFromRpc', function () {
|
||||
it('should work', async function () {
|
||||
const gas: number = await oracle.legacy.fetchGasPriceFromRpc()
|
||||
gas.should.be.a('number')
|
||||
gas.should.be.above(1)
|
||||
gas.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should work with custom rpc', async function () {
|
||||
const rpc = INJECTED_RPC_URL
|
||||
const oracle = new GasPriceOracle({ defaultRpc: rpc })
|
||||
oracle.legacy.configuration.defaultRpc.should.be.equal(rpc)
|
||||
const gas: number = await oracle.legacy.fetchGasPriceFromRpc()
|
||||
|
||||
gas.should.be.a('number')
|
||||
|
||||
gas.should.be.above(1)
|
||||
gas.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should throw if default rpc is down', async function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
const { GasPriceOracle } = require('../index')
|
||||
|
||||
oracle = new GasPriceOracle()
|
||||
// @ts-ignore
|
||||
await oracle.legacy.fetchGasPriceFromRpc().should.be.rejectedWith('Default RPC is down. Probably a network error.')
|
||||
mockery.disable()
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy gasPrice', function () {
|
||||
it('should work', async function () {
|
||||
const gas = (await oracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
|
||||
|
||||
gas.instant.should.be.a('number')
|
||||
gas.fast.should.be.a('number')
|
||||
gas.standard.should.be.a('number')
|
||||
gas.low.should.be.a('number')
|
||||
|
||||
gas.instant.should.be.at.least(gas.fast)
|
||||
gas.fast.should.be.at.least(gas.standard)
|
||||
gas.standard.should.be.at.least(gas.low)
|
||||
gas.low.should.not.be.equal(0)
|
||||
})
|
||||
|
||||
it('should fallback', async function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
const { GasPriceOracle } = require('../index')
|
||||
|
||||
oracle = new GasPriceOracle()
|
||||
|
||||
const gas = (await oracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
|
||||
|
||||
const shouldBe = LegacyGasPriceOracle.getCategorize(NETWORKS[ChainId.MAINNET].defaultGasPrice)
|
||||
|
||||
gas.instant.should.be.equal(shouldBe.instant)
|
||||
gas.fast.should.be.equal(shouldBe.fast)
|
||||
gas.standard.should.be.equal(shouldBe.standard)
|
||||
gas.low.should.be.equal(shouldBe.low)
|
||||
|
||||
mockery.disable()
|
||||
})
|
||||
|
||||
it('should fallback to set values', async function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
const { GasPriceOracle } = require('../index')
|
||||
oracle = new GasPriceOracle()
|
||||
|
||||
const fallbackGasPrices = {
|
||||
gasPrices: {
|
||||
instant: 50,
|
||||
fast: 21,
|
||||
standard: 10,
|
||||
low: 3,
|
||||
},
|
||||
}
|
||||
const gas = (await oracle.gasPrices({ isLegacy: true, fallbackGasPrices })) as unknown as GasPrice
|
||||
|
||||
gas.instant.should.be.equal(fallbackGasPrices.gasPrices.instant)
|
||||
gas.fast.should.be.equal(fallbackGasPrices.gasPrices.fast)
|
||||
gas.standard.should.be.equal(fallbackGasPrices.gasPrices.standard)
|
||||
gas.low.should.be.equal(fallbackGasPrices.gasPrices.low)
|
||||
|
||||
mockery.disable()
|
||||
})
|
||||
|
||||
it('should cache', async function () {
|
||||
const oracle = new GasPriceOracle({ shouldCache: true })
|
||||
const gasPricesFirst = await oracle.legacy.gasPrices()
|
||||
|
||||
await sleep(2000)
|
||||
const gasPricesSecond = await oracle.legacy.gasPrices()
|
||||
|
||||
if (gasPricesFirst.fast) {
|
||||
gasPricesFirst.fast.should.be.at.equal(gasPricesSecond?.fast)
|
||||
}
|
||||
|
||||
await sleep(4000)
|
||||
const gasPricesThird = await oracle.legacy.gasPrices()
|
||||
|
||||
if (gasPricesSecond.fast) {
|
||||
gasPricesSecond.fast.should.be.at.equal(gasPricesThird?.fast)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('median', function () {
|
||||
it('should work', function () {
|
||||
const gas1 = { instant: 100, fast: 100, standard: 100, low: 100 }
|
||||
const gas2 = { instant: 90, fast: 90, standard: 90, low: 90 }
|
||||
const gas3 = { instant: 70, fast: 70, standard: 70, low: 70 }
|
||||
const gas4 = { instant: 110.1, fast: 110.1, standard: 110.1, low: 110.1 }
|
||||
|
||||
let gas: GasPrice = LegacyGasPriceOracle.getMedianGasPrice([gas1, gas2, gas3])
|
||||
|
||||
gas.instant.should.be.a('number')
|
||||
gas.fast.should.be.a('number')
|
||||
gas.standard.should.be.a('number')
|
||||
gas.low.should.be.a('number')
|
||||
|
||||
gas.instant.should.be.eq(90)
|
||||
gas.fast.should.be.eq(90)
|
||||
gas.standard.should.be.eq(90)
|
||||
gas.low.should.be.eq(90)
|
||||
|
||||
gas = LegacyGasPriceOracle.getMedianGasPrice([gas1, gas2, gas3, gas4])
|
||||
|
||||
gas.instant.should.be.a('number')
|
||||
gas.fast.should.be.a('number')
|
||||
gas.standard.should.be.a('number')
|
||||
gas.low.should.be.a('number')
|
||||
|
||||
gas.instant.should.be.eq(95)
|
||||
gas.fast.should.be.eq(95)
|
||||
gas.standard.should.be.eq(95)
|
||||
gas.low.should.be.eq(95)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchMedianGasPriceOffChain', function () {
|
||||
it('should work', async function () {
|
||||
const gas: GasPrice = await oracle.legacy.fetchMedianGasPriceOffChain()
|
||||
|
||||
gas.instant.should.be.a('number')
|
||||
gas.fast.should.be.a('number')
|
||||
gas.standard.should.be.a('number')
|
||||
gas.low.should.be.a('number')
|
||||
|
||||
gas.instant.should.be.at.least(gas.fast)
|
||||
gas.fast.should.be.at.least(gas.standard)
|
||||
gas.standard.should.be.at.least(gas.low)
|
||||
gas.low.should.not.be.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalize result values', function () {
|
||||
const wrongDecimalsGas = {
|
||||
gasPrices: {
|
||||
instant: 1.1,
|
||||
fast: 2.12345678901,
|
||||
standard: 3.12345678901,
|
||||
low: 3.12345679,
|
||||
},
|
||||
}
|
||||
|
||||
const checkDecimals = (gas: GasPrice) => {
|
||||
const gasPrices: number[] = Object.values(gas)
|
||||
|
||||
for (const gas of gasPrices) {
|
||||
new BigNumber(gas).dp().should.be.at.most(9)
|
||||
}
|
||||
}
|
||||
|
||||
it('default fallback should be normalized', function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
|
||||
oracle = new GasPriceOracle({ fallbackGasPrices: wrongDecimalsGas })
|
||||
|
||||
checkDecimals(oracle.legacy.configuration.fallbackGasPrices)
|
||||
|
||||
mockery.disable()
|
||||
})
|
||||
|
||||
it('fallback should be normalized', async function () {
|
||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||
const { GasPriceOracle } = require('../index')
|
||||
|
||||
oracle = new GasPriceOracle()
|
||||
|
||||
const gas = await oracle.legacy.gasPrices(wrongDecimalsGas.gasPrices)
|
||||
|
||||
checkDecimals(gas)
|
||||
mockery.disable()
|
||||
})
|
||||
|
||||
it('rpc fallback should be normalized', async function () {
|
||||
oracle = new GasPriceOracle({ chainId: 42161, defaultRpc: 'https://arb1.arbitrum.io/rpc' })
|
||||
|
||||
const gas = await oracle.legacy.gasPrices()
|
||||
|
||||
checkDecimals(gas)
|
||||
})
|
||||
})
|
||||
|
||||
describe('askOracle', function () {
|
||||
const chains = Object.keys(NETWORKS).map((id) => Number(id))
|
||||
|
||||
chains.forEach((chainId) => {
|
||||
describe(`all ${ChainId[chainId]} oracles should answer`, function () {
|
||||
oracle = new GasPriceOracle({ chainId })
|
||||
;({ offChainOracles } = oracle.legacy)
|
||||
|
||||
for (const o of Object.values(offChainOracles) as OffChainOracle[]) {
|
||||
it(`check ${o.name}`, async function () {
|
||||
try {
|
||||
const gas: GasPrice = await oracle.legacy.askOracle(o)
|
||||
|
||||
gas.instant.should.be.a('number')
|
||||
gas.fast.should.be.a('number')
|
||||
gas.standard.should.be.a('number')
|
||||
gas.low.should.be.a('number')
|
||||
|
||||
gas.instant.should.be.at.least(gas.fast)
|
||||
gas.fast.should.be.at.least(gas.standard)
|
||||
gas.standard.should.be.at.least(gas.low)
|
||||
gas.low.should.not.be.equal(0)
|
||||
} catch (e) {
|
||||
console.error(`Failed to get data from ${o.name} oracle`)
|
||||
throw new Error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after('after', function () {
|
||||
after(function () {
|
||||
mockery.disable()
|
||||
mockery.deregisterMock('node-fetch')
|
||||
})
|
||||
})
|
||||
40
@tornado/gas-price-oracle/src/types.ts
Normal file
40
@tornado/gas-price-oracle/src/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { AllOracles } from './services/legacy-gas-price'
|
||||
|
||||
export interface Block {
|
||||
baseFeePerGas: string
|
||||
difficulty: string
|
||||
extraData: string
|
||||
gasLimit: string
|
||||
gasUsed: string
|
||||
hash: string
|
||||
miner: string
|
||||
mixHash: string
|
||||
nonce: string
|
||||
number: string
|
||||
parentHash: string
|
||||
receiptsRoot: string
|
||||
sha3Uncles: string
|
||||
size: string
|
||||
stateRoot: string
|
||||
timestamp: string
|
||||
totalDifficulty: string
|
||||
transactions: unknown[]
|
||||
transactionsRoot: string
|
||||
uncles: unknown[]
|
||||
}
|
||||
|
||||
export interface FeeHistory {
|
||||
baseFeePerGas: string[]
|
||||
gasUsedRatio: number[]
|
||||
reward?: string[][]
|
||||
oldestBlock: string
|
||||
}
|
||||
|
||||
export type NetworksConfig = {
|
||||
oracles: AllOracles
|
||||
rpcUrl: string
|
||||
defaultGasPrice: number
|
||||
maxGasPrice: number
|
||||
blocksCount: number
|
||||
percentile: number
|
||||
}
|
||||
20
@tornado/gas-price-oracle/src/utils/crypto.ts
Normal file
20
@tornado/gas-price-oracle/src/utils/crypto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { GWEI, GWEI_PRECISION } from '@/constants'
|
||||
|
||||
const toGwei = (amount: number | string | BigNumber): BigNumber => {
|
||||
return new BigNumber(amount).multipliedBy(GWEI).decimalPlaces(GWEI_PRECISION)
|
||||
}
|
||||
|
||||
const fromWeiToGwei = (amount: number | string | BigNumber): BigNumber => {
|
||||
return new BigNumber(amount).dividedBy(GWEI).decimalPlaces(GWEI_PRECISION)
|
||||
}
|
||||
|
||||
const fromNumberToHex = (amount: number | string | BigNumber): string => {
|
||||
return `0x${new BigNumber(amount).toString(16)}`
|
||||
}
|
||||
|
||||
const fromGweiToWeiHex = (value: number): string => {
|
||||
return fromNumberToHex(new BigNumber(value).multipliedBy(GWEI).decimalPlaces(0))
|
||||
}
|
||||
|
||||
export { toGwei, fromWeiToGwei, fromGweiToWeiHex, fromNumberToHex }
|
||||
47
@tornado/gas-price-oracle/src/utils/index.ts
Normal file
47
@tornado/gas-price-oracle/src/utils/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export * from './math'
|
||||
export * from './crypto'
|
||||
|
||||
const sleep = (time: number): Promise<boolean> => {
|
||||
return new Promise((res) => setTimeout(() => res(true), time))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recursively resolved value of the object's subproperty given the properties path, separated by dots.
|
||||
* If properties path is null, undefined or an empty string - returns the object itself
|
||||
* @param obj - The object, from which get property or subproperty
|
||||
* @param propertiesInString - Property name or properties chained string, like 'user.email.domain'
|
||||
* @returns The value of the subproperty by path or object itself
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ### Object
|
||||
* ```ts
|
||||
* const x = {
|
||||
* y: {
|
||||
* z: 11
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* ### Property path in object
|
||||
* ```ts
|
||||
* const propertyPath = 'y.z';
|
||||
* ```
|
||||
*
|
||||
* ### Usage
|
||||
* ```ts
|
||||
* const subpropertyValue = resolvePropertyPath(x, propertyPath);
|
||||
* ```
|
||||
*
|
||||
* ### Result (subproperty value)
|
||||
* ```ts
|
||||
* 11
|
||||
* ```
|
||||
*/
|
||||
const resolvePropertyPath = (obj: object, propertyPath: string | undefined | null): any => {
|
||||
const properties = propertyPath?.split('.') || []
|
||||
|
||||
return properties.reduce((curr: { [key: string]: any }, nextProperty: string) => curr[nextProperty], obj)
|
||||
}
|
||||
|
||||
export { sleep, resolvePropertyPath }
|
||||
45
@tornado/gas-price-oracle/src/utils/math.ts
Normal file
45
@tornado/gas-price-oracle/src/utils/math.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import { BG_ZERO, PERCENT_MULTIPLIER } from '@/constants'
|
||||
|
||||
import { toGwei } from './crypto'
|
||||
|
||||
type FindMaxRes = {
|
||||
highest: BigNumber
|
||||
index: number
|
||||
}
|
||||
|
||||
const findMax = (values: BigNumber[]): FindMaxRes => {
|
||||
return values.reduce(
|
||||
(acc, curr, index) => {
|
||||
const isGreaterThanAcc = curr.isGreaterThan(acc.highest)
|
||||
if (isGreaterThanAcc) {
|
||||
acc.highest = curr
|
||||
acc.index = index
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{
|
||||
highest: BG_ZERO,
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const getMedian = <T>(arr: T[]): number => {
|
||||
return Math.floor(arr.length / 2)
|
||||
}
|
||||
|
||||
const round = (value: number | string | BigNumber): BigNumber => {
|
||||
return new BigNumber(value).decimalPlaces(0, 2)
|
||||
}
|
||||
|
||||
const roundGwei = (value: number | string | BigNumber): BigNumber => {
|
||||
return toGwei(value).decimalPlaces(0, 2)
|
||||
}
|
||||
|
||||
const bumpOnPercent = (value: number, bumpPercent: number): number => {
|
||||
return value + (value * bumpPercent) / PERCENT_MULTIPLIER
|
||||
}
|
||||
|
||||
export { findMax, getMedian, round, roundGwei, bumpOnPercent }
|
||||
9
@tornado/gas-price-oracle/tsconfig.esm.json
Normal file
9
@tornado/gas-price-oracle/tsconfig.esm.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"target": "es2015",
|
||||
"module": "es2015",
|
||||
"outDir": "lib/esm"
|
||||
}
|
||||
}
|
||||
33
@tornado/gas-price-oracle/tsconfig.json
Normal file
33
@tornado/gas-price-oracle/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es5", "es6", "es2021", "esnext"],
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "lib",
|
||||
"noImplicitAny": true,
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"checkJs": false,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictFunctionTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "src/tests"]
|
||||
}
|
||||
@@ -23,9 +23,10 @@
|
||||
],
|
||||
"author": "Jordi Baylina",
|
||||
"license": "GPL-3.0",
|
||||
"homepage": "https://git.tornado.ws/tornado-packages/snarkjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://development.tornadocash.community/tornadocash/snarkjs"
|
||||
"url": "https://git.tornado.ws/tornado-packages/archive-monorepo"
|
||||
},
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.43",
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
{
|
||||
"name": "@tornado/web3-providers-http",
|
||||
"version": "1.6.5-p1",
|
||||
"description": "Module to handle web3 RPC connections over HTTP.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ethereum/web3.js/tree/1.x/packages/web3-providers-http"
|
||||
},
|
||||
"license": "LGPL-3.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc -b tsconfig.json",
|
||||
"dtslint": "dtslint --localTs ../../node_modules/typescript/lib types"
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"@tornado/xhr2-cookies": "workspace:*",
|
||||
"web3-core-helpers": "1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dtslint": "^3.4.1",
|
||||
"typescript": "^3.9.5"
|
||||
}
|
||||
"name": "@tornado/web3-providers-http",
|
||||
"version": "1.6.5-p1",
|
||||
"description": "Module to handle web3 RPC connections over HTTP.",
|
||||
"homepage": "https://git.tornado.ws/tornado-packages/archive-monorepo/src/branch/main/@tornado/web3-providers-http",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.tornado.ws/tornado-packages/archive-monorepo"
|
||||
},
|
||||
"license": "LGPL-3.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc -b tsconfig.json",
|
||||
"dtslint": "dtslint --localTs ../../node_modules/typescript/lib types"
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"@tornado/xhr2-cookies": "workspace:*",
|
||||
"web3-core-helpers": "1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dtslint": "^3.4.1",
|
||||
"typescript": "^3.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
3494
@tornado/websnark/package-lock.json
generated
3494
@tornado/websnark/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,10 @@
|
||||
],
|
||||
"author": "Jordi Baylina",
|
||||
"license": "GPL-3.0",
|
||||
"homepage": "https://git.tornado.ws/tornado-packages/websnark",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://development.tornadocash.community/tornadocash/websnark"
|
||||
"url": "https://git.tornado.ws/tornado-packages/archive-monorepo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^16.2.3",
|
||||
|
||||
@@ -1,51 +1,52 @@
|
||||
{
|
||||
"name": "@tornado/xhr2-cookies",
|
||||
"version": "1.1.0-p0",
|
||||
"author": "Ionut Costica <ionut.costica@gmail.com>",
|
||||
"license": "MIT",
|
||||
"description": "XMLHttpRequest polyfill for node.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/souldreamer/xhr2-cookies.git"
|
||||
},
|
||||
"keywords": [
|
||||
"XMLHttpRequest",
|
||||
"cookies",
|
||||
"xhr2"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"cookiejar": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.16.8",
|
||||
"@types/cookie-parser": "^1.4.1",
|
||||
"@types/express": "^4.0.39",
|
||||
"@types/morgan": "^1.7.35",
|
||||
"@types/node": "^6",
|
||||
"ava": "^0.23.0",
|
||||
"ava-ts": "^0.23.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"express": "^4.16.2",
|
||||
"morgan": "^1.9.0",
|
||||
"ts-loader": "^2.3.4",
|
||||
"ts-node": "^3.3.0",
|
||||
"typescript": "^2.5.2",
|
||||
"webpack": "^3.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsc",
|
||||
"test": "tsc -p ./test && ava-ts -v"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
"test/*.spec.ts"
|
||||
],
|
||||
"source": [
|
||||
"*.ts",
|
||||
"!dist/**/*"
|
||||
]
|
||||
}
|
||||
"name": "@tornado/xhr2-cookies",
|
||||
"version": "1.1.0-p0",
|
||||
"author": "Ionut Costica <ionut.costica@gmail.com>",
|
||||
"license": "MIT",
|
||||
"description": "XMLHttpRequest polyfill for node.js",
|
||||
"homepage": "https://github.com/souldreamer/xhr2-cookies",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.tornado.ws/tornado-packages/archive-monorepo"
|
||||
},
|
||||
"keywords": [
|
||||
"XMLHttpRequest",
|
||||
"cookies",
|
||||
"xhr2"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"cookiejar": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.16.8",
|
||||
"@types/cookie-parser": "^1.4.1",
|
||||
"@types/express": "^4.0.39",
|
||||
"@types/morgan": "^1.7.35",
|
||||
"@types/node": "^6",
|
||||
"ava": "^0.23.0",
|
||||
"ava-ts": "^0.23.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"express": "^4.16.2",
|
||||
"morgan": "^1.9.0",
|
||||
"ts-loader": "^2.3.4",
|
||||
"ts-node": "^3.3.0",
|
||||
"typescript": "^2.5.2",
|
||||
"webpack": "^3.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsc",
|
||||
"test": "tsc -p ./test && ava-ts -v"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
"test/*.spec.ts"
|
||||
],
|
||||
"source": [
|
||||
"*.ts",
|
||||
"!dist/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"name": "archive-monorepo",
|
||||
"packageManager": "yarn@3.5.0",
|
||||
"packageManager": "yarn@3.6.1",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"@tornado/*"
|
||||
],
|
||||
"scripts": {
|
||||
"install:linux": "source .env && yarn",
|
||||
"install:windows": "call .env.bat && yarn",
|
||||
"git:publish:linux": "source .env && yarn workspaces foreach npm publish",
|
||||
"git:publish:windows": "call .env.bat && yarn workspaces foreach npm publish",
|
||||
"git:status": "ls -1 @tornado | xargs -I repl git -C @tornado/repl status",
|
||||
"git:add": "ls -1 @tornado | xargs -I repl git -C @tornado/repl add .",
|
||||
"git:commit": "ls -1 @tornado | xargs -I repl git -C @tornado/repl commit -sm ",
|
||||
|
||||
Reference in New Issue
Block a user