Initial Commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules
|
||||
difficulty-chart.html
|
||||
difficulty-series.json
|
||||
examples.json
|
||||
603
package-lock.json
generated
Normal file
603
package-lock.json
generated
Normal file
@@ -0,0 +1,603 @@
|
||||
{
|
||||
"name": "ss-filter",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ss-filter",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@bitauth/libauth": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@bitauth/libauth": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@bitauth/libauth/-/libauth-3.0.0.tgz",
|
||||
"integrity": "sha512-3yoL31XpnhAnf5nDVMFk4xPqebxDwXrgYAwpa31ARJnV5A/eXWlpNYvCd6FTZPFM4VvKfjCBi+jRCrw1hOZ0Jg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
|
||||
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "ss-filter",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"generate-examples": "tsx src/generate-examples.ts",
|
||||
"graph": "tsx src/index.ts",
|
||||
"start": "npm run graph",
|
||||
"build-graph": "npm run generate-examples && npm run graph",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bitauth/libauth": "^3.0.0"
|
||||
}
|
||||
}
|
||||
74
src/example.ts
Normal file
74
src/example.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
export const contractData = {
|
||||
version: 'AnyHedge v0.12',
|
||||
address: 'bitcoincash:pwym7ha7gt7k3h36wp735yhsxp00ayh7cxvrzls43xf4j7zs9s3fuynsxzv6y',
|
||||
parameters: {
|
||||
maturityTimestamp: 1771228572n,
|
||||
startTimestamp: 1771220472n,
|
||||
highLiquidationPrice: 111200n,
|
||||
lowLiquidationPrice: 44480n,
|
||||
payoutSats: 134892n,
|
||||
nominalUnitsXSatsPerBch: 9999993600n,
|
||||
satsForNominalUnitsAtHighLiquidation: 89928n,
|
||||
oraclePublicKey: '02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818',
|
||||
longLockScript: '76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac',
|
||||
shortLockScript: '76a91423e32af4b40eb093f19bd00f34f74862aa5aa45c88ac',
|
||||
enableMutualRedemption: 0n,
|
||||
longMutualRedeemPublicKey: '023021d3e1ec7943e54f56d163e4551c2be01d5f5cd933a86d2f6b81f53d64bf72',
|
||||
shortMutualRedeemPublicKey: '02275d08908cf337c1b4b74a78b9d31d258ff3469b5a9e392dd4cb774d8c563991'
|
||||
},
|
||||
metadata: {
|
||||
takerSide: 'long',
|
||||
makerSide: 'short',
|
||||
shortPayoutAddress: 'bitcoincash:qq37x2h5ks8tpyl3n0gq7d8hfp325k4ytsuvqzna24',
|
||||
longPayoutAddress: 'bitcoincash:qp6uw90vkaqh3l58jvl90628uh5jmyztsykm722fln',
|
||||
startingOracleMessage: 'f8ad9269e76e1700cd6e170030d90000',
|
||||
startingOracleSignature: 'f48e57e850e8d7235beb317526e5a5e17358df8576d146a6c0a0a0c18f26af6dba56848a60990f5314649f5294a8a50f65b401b9fbb2aa126c6208cc9f5bc9d3',
|
||||
durationInSeconds: 8100n,
|
||||
highLiquidationPriceMultiplier: 2,
|
||||
lowLiquidationPriceMultiplier: 0.8,
|
||||
isSimpleHedge: 0n,
|
||||
startPrice: 55600n,
|
||||
nominalUnits: 99.999936,
|
||||
shortInputInOracleUnits: 49.999968,
|
||||
longInputInOracleUnits: 24.999984,
|
||||
shortInputInSatoshis: 89928n,
|
||||
longInputInSatoshis: 44964n,
|
||||
minerCostInSatoshis: 638n
|
||||
},
|
||||
fundings: [
|
||||
{
|
||||
fundingTransactionHash: '278b56d2b067001eadad9e89fad1de1cd1b425cdc1cc6f9c53658e909555957d',
|
||||
fundingOutputIndex: 0n,
|
||||
fundingSatoshis: 136862n,
|
||||
settlement: [Object]
|
||||
}
|
||||
],
|
||||
fees: [
|
||||
{
|
||||
address: 'bitcoincash:qq37x2h5ks8tpyl3n0gq7d8hfp325k4ytsuvqzna24',
|
||||
satoshis: 2666n,
|
||||
name: 'Liquidity premium',
|
||||
description: 'Provides counterparty liquidity at a premium.'
|
||||
},
|
||||
{
|
||||
address: 'bitcoincash:qzduxz0ancq5m2ucxj6y5kkj9rnl9p5t5yu964fu9z',
|
||||
satoshis: 1332n,
|
||||
name: 'Settlement Service Fee',
|
||||
description: 'Validates contract parameters and then monitors the contracts oracle prices and automates settlement.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
export const automatedPayoutData = {
|
||||
settlementType: 'maturation',
|
||||
settlementTransactionHash: 'a18f82d373a67c908896399da0ee64de27a237d0089a9547de8806db3890737a',
|
||||
shortPayoutInSatoshis: 90492n,
|
||||
longPayoutInSatoshis: 44400n,
|
||||
settlementTimestamp: 1771228572n,
|
||||
settlementMessage: '9ecd92696e6f1700546f170082d80000',
|
||||
settlementSignature: '7160d776b3cb9a601bdb85a12fb010457c5c26d3e15af861a3e4b61b96591a7485ff50833e3f03cd4279b62c1d592c043e951a2c0c74323d4a586a14d621c87e',
|
||||
previousMessage: '62cd92696d6f1700536f170082d80000',
|
||||
previousSignature: '14dc631ee6dc00b39f1a2847516ad9c8b5c3d6ef6e1c4ac672ad53508eb631991a0fb0284e481a4d10bc5a80d94f8cc5622e81257f2c603644898471cfd1553e',
|
||||
settlementPrice: 55426n
|
||||
}
|
||||
124
src/ext-json.ts
Normal file
124
src/ext-json.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* TODO: These are intended as temporary stand-ins until this functionality has been implemented directly in LibAuth.
|
||||
* We are doing this so that we may better standardize with the rest of the BCH eco-system in future.
|
||||
* See: https://github.com/bitauth/libauth/pull/108
|
||||
*/
|
||||
|
||||
import { binToHex, hexToBin } from '@bitauth/libauth';
|
||||
|
||||
export const extendedJsonReplacer = function (value: any): any {
|
||||
if (typeof value === 'bigint') {
|
||||
return `<bigint: ${value.toString()}n>`;
|
||||
} else if (value instanceof Uint8Array) {
|
||||
return `<Uint8Array: ${binToHex(value)}>`;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const extendedJsonReviver = function (value: any): any {
|
||||
// Define RegEx that matches our Extended JSON fields.
|
||||
const bigIntRegex = /^<bigint: (?<bigint>[+-]?[0-9]*)n>$/;
|
||||
const uint8ArrayRegex = /^<Uint8Array: (?<hex>[a-f0-9]*)>$/;
|
||||
|
||||
// Only perform a check if the value is a string.
|
||||
// NOTE: We can skip all other values as all Extended JSON encoded fields WILL be a string.
|
||||
if (typeof value === 'string') {
|
||||
// Check if this value matches an Extended JSON encoded bigint.
|
||||
const bigintMatch = value.match(bigIntRegex);
|
||||
if (bigintMatch) {
|
||||
// Access the named group directly instead of using array indices
|
||||
const { bigint } = bigintMatch.groups!;
|
||||
|
||||
// Return the value casted to bigint.
|
||||
return BigInt(bigint);
|
||||
}
|
||||
|
||||
const uint8ArrayMatch = value.match(uint8ArrayRegex);
|
||||
if (uint8ArrayMatch) {
|
||||
// Access the named group directly instead of using array indices
|
||||
const { hex } = uint8ArrayMatch.groups!;
|
||||
|
||||
// Return the value casted to bigint.
|
||||
return hexToBin(hex);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the original value.
|
||||
return value;
|
||||
};
|
||||
|
||||
export const encodeExtendedJsonObject = function (value: any): any {
|
||||
// If this is an object type (and it is not null - which is technically an "object")...
|
||||
// ... and it is not an ArrayBuffer (e.g. Uint8Array) which is also technically an "object...
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!ArrayBuffer.isView(value)
|
||||
) {
|
||||
// If this is an array, recursively call this function on each value.
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(encodeExtendedJsonObject);
|
||||
}
|
||||
|
||||
// Declare object to store extended JSON entries.
|
||||
const encodedObject: any = {};
|
||||
|
||||
// Iterate through each entry and encode it to extended JSON.
|
||||
for (const [key, valueToEncode] of Object.entries(value)) {
|
||||
encodedObject[key] = encodeExtendedJsonObject(valueToEncode);
|
||||
}
|
||||
|
||||
// Return the extended JSON encoded object.
|
||||
return encodedObject;
|
||||
}
|
||||
|
||||
// Return the replaced value.
|
||||
return extendedJsonReplacer(value);
|
||||
};
|
||||
|
||||
export const decodeExtendedJsonObject = function (value: any): any {
|
||||
// If this is an object type (and it is not null - which is technically an "object")...
|
||||
// ... and it is not an ArrayBuffer (e.g. Uint8Array) which is also technically an "object...
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!ArrayBuffer.isView(value)
|
||||
) {
|
||||
// If this is an array, recursively call this function on each value.
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(decodeExtendedJsonObject);
|
||||
}
|
||||
|
||||
// Declare object to store decoded JSON entries.
|
||||
const decodedObject: any = {};
|
||||
|
||||
// Iterate through each entry and decode it from extended JSON.
|
||||
for (const [key, valueToEncode] of Object.entries(value)) {
|
||||
decodedObject[key] = decodeExtendedJsonObject(valueToEncode);
|
||||
}
|
||||
|
||||
// Return the extended JSON encoded object.
|
||||
return decodedObject;
|
||||
}
|
||||
|
||||
// Return the revived value.
|
||||
return extendedJsonReviver(value);
|
||||
};
|
||||
|
||||
export const encodeExtendedJson = function (
|
||||
value: any,
|
||||
space: number | undefined = undefined,
|
||||
): string {
|
||||
const replacedObject = encodeExtendedJsonObject(value);
|
||||
const stringifiedObject = JSON.stringify(replacedObject, null, space);
|
||||
|
||||
return stringifiedObject;
|
||||
};
|
||||
|
||||
export const decodeExtendedJson = function (json: string): any {
|
||||
const parsedObject = JSON.parse(json);
|
||||
const revivedObject = decodeExtendedJsonObject(parsedObject);
|
||||
|
||||
return revivedObject;
|
||||
};
|
||||
53
src/filter.d.ts
vendored
Normal file
53
src/filter.d.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface SettlementSummary {
|
||||
liquidated: boolean;
|
||||
inputSats: bigint;
|
||||
outputSats: bigint;
|
||||
durationInSeconds: number;
|
||||
takerLeverage: number;
|
||||
makerLeverage: number;
|
||||
percentageChange: number;
|
||||
oraclePublicKey: string;
|
||||
settlementTimestamp: number;
|
||||
}
|
||||
|
||||
export interface InterestingContractResult {
|
||||
isInteresting: boolean;
|
||||
reason?: string;
|
||||
summary: SettlementSummary;
|
||||
}
|
||||
|
||||
export interface InterestingSettlementFilterOptions {
|
||||
difficultyFilterExponent?: number;
|
||||
difficultyAdjustmentInterval?: number;
|
||||
baseLiquidationThresholdSats?: bigint;
|
||||
basePercentageThreshold?: number;
|
||||
difficultyIncrementRate?: number;
|
||||
minDifficultyFilterExponent?: number;
|
||||
maxDifficultyFilterExponent?: number;
|
||||
}
|
||||
|
||||
export class InterestingSettlementFilter {
|
||||
difficultyFilterExponent: number;
|
||||
difficultyIncrementRate: number;
|
||||
minDifficultyFilterExponent: number;
|
||||
maxDifficultyFilterExponent: number;
|
||||
|
||||
difficultyAdjustmentInterval: number;
|
||||
|
||||
baseLiquidationThresholdSats: bigint;
|
||||
basePercentageThreshold: number;
|
||||
|
||||
public constructor(filterOptions?: InterestingSettlementFilterOptions);
|
||||
public start(): void;
|
||||
public stop(): void;
|
||||
public incrementDifficulty(steps?: number): void;
|
||||
public decrementDifficulty(steps?: number): void;
|
||||
public getContractOutcome(
|
||||
contractData: Record<string, unknown>,
|
||||
automatedPayoutData: Record<string, unknown>,
|
||||
): SettlementSummary;
|
||||
public isInterestingSettlement(
|
||||
contractData: Record<string, unknown>,
|
||||
automatedPayoutData: Record<string, unknown>,
|
||||
): InterestingContractResult;
|
||||
}
|
||||
315
src/filter.js
Normal file
315
src/filter.js
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* @typedef {Object} InterestingContractResult
|
||||
*
|
||||
* @property {boolean} isInteresting
|
||||
* @property {string | undefined} reason}
|
||||
* @property {SettlementSummary} summary
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SettlementSummary
|
||||
*
|
||||
* @property {boolean} liquidated
|
||||
* @property {bigint} inputSats
|
||||
* @property {bigint} outputSats
|
||||
* @property {number} durationInSeconds
|
||||
* @property {number} takerLeverage
|
||||
* @property {number} makerLeverage
|
||||
* @property {number} percentageChange
|
||||
* @property {string} oraclePublicKey
|
||||
* @property {number} settlementTimestamp
|
||||
* @property {string | undefined} reason}
|
||||
*/
|
||||
|
||||
export class InterestingSettlementFilter {
|
||||
/**
|
||||
* The base BCH threshold in satoshis for liquidation filter.
|
||||
* Contracts with total payout above this threshold are considered interesting.
|
||||
*
|
||||
* // Default is 1 BCH
|
||||
* @default 100_000_000n
|
||||
* @type {bigint}
|
||||
*/
|
||||
baseLiquidationThresholdSats = 100_000_000n;
|
||||
|
||||
/**
|
||||
* The base percentage threshold for payout difference filter.
|
||||
* Contracts with payout difference above this percentage are considered interesting.
|
||||
*
|
||||
* // Default is 5%
|
||||
* @default 5
|
||||
* @type {number}
|
||||
*/
|
||||
basePercentageThreshold = 20;
|
||||
|
||||
/**
|
||||
* The rate at which difficulty increases when an interesting settlement is found.
|
||||
*
|
||||
* // Default is 5%
|
||||
* @default 0.05
|
||||
* @type {number}
|
||||
*/
|
||||
difficultyIncrementRate = 0.05;
|
||||
|
||||
/**
|
||||
* An exponent that controls the "difficulty" of a contract to be considered interesting.
|
||||
* This is increased every time a contract is considered interesting, and decreased over time.
|
||||
* This prevents us from spamming clients with settlements if there is an event that causes a large amount of settlements.
|
||||
*/
|
||||
difficultyFilterExponent = 1.0;
|
||||
|
||||
/**
|
||||
* The minimum difficulty filter exponent.
|
||||
* This is the minimum value that the difficulty filter exponent can reach.
|
||||
* @default 0.1
|
||||
* @type {number}
|
||||
*/
|
||||
minDifficultyFilterExponent = 0.4;
|
||||
|
||||
/**
|
||||
* The maximum difficulty filter exponent.
|
||||
* This is the maximum value that the difficulty filter exponent can reach.
|
||||
* @default 100
|
||||
* @type {number}
|
||||
*/
|
||||
maxDifficultyFilterExponent = 5;
|
||||
|
||||
/**
|
||||
* A timer that adjusts the difficulty of a contract to be considered interesting.
|
||||
*/
|
||||
difficultyAdjustmentTimer = null;
|
||||
|
||||
/**
|
||||
* The time interval in milliseconds between difficulty adjustments.
|
||||
*/
|
||||
difficultyAdjustmentInterval = 1000 * 60 * 2 // 2 minutes
|
||||
|
||||
|
||||
/**
|
||||
* The constructor for the InterestingSettlementFilter.
|
||||
*/
|
||||
constructor(filterOptions = {}) {
|
||||
// Exponential values
|
||||
this.difficultyFilterExponent = filterOptions.difficultyFilterExponent ?? this.difficultyFilterExponent;
|
||||
this.minDifficultyFilterExponent = filterOptions.minDifficultyFilterExponent ?? this.minDifficultyFilterExponent;
|
||||
this.maxDifficultyFilterExponent = filterOptions.maxDifficultyFilterExponent ?? this.maxDifficultyFilterExponent;
|
||||
this.difficultyIncrementRate = filterOptions.difficultyIncrementRate ?? this.difficultyIncrementRate;
|
||||
|
||||
// Interval
|
||||
this.difficultyAdjustmentInterval = filterOptions.difficultyAdjustmentInterval ?? this.difficultyAdjustmentInterval;
|
||||
|
||||
// Base values
|
||||
this.baseLiquidationThresholdSats = filterOptions.baseLiquidationThresholdSats ?? this.baseLiquidationThresholdSats;
|
||||
this.basePercentageThreshold = filterOptions.basePercentageThreshold ?? this.basePercentageThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the filter
|
||||
*/
|
||||
start() {
|
||||
this.startDifficultyAdjustmentTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the filter
|
||||
*/
|
||||
stop() {
|
||||
this.stopDifficultyAdjustmentTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the difficulty adjustment timer.
|
||||
*/
|
||||
startDifficultyAdjustmentTimer() {
|
||||
this.difficultyAdjustmentTimer = setInterval(() => {
|
||||
this.decrementDifficulty();
|
||||
}, this.difficultyAdjustmentInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the difficulty adjustment timer.
|
||||
*/
|
||||
stopDifficultyAdjustmentTimer() {
|
||||
if (this.difficultyAdjustmentTimer) {
|
||||
clearInterval(this.difficultyAdjustmentTimer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the difficulty of a contract to be considered interesting.
|
||||
*/
|
||||
incrementDifficulty(steps = 1) {
|
||||
for (let i = 0; i < steps; i++) {
|
||||
this.difficultyFilterExponent = Math.min(
|
||||
this.maxDifficultyFilterExponent,
|
||||
Number(this.difficultyFilterExponent) * (1 + Number(this.difficultyIncrementRate))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the difficulty of a contract to be considered interesting.
|
||||
*/
|
||||
decrementDifficulty(steps = 1) {
|
||||
for (let i = 0; i < steps; i++) {
|
||||
this.difficultyFilterExponent = Math.max(
|
||||
this.minDifficultyFilterExponent,
|
||||
Number(this.difficultyFilterExponent) * (1 - Number(this.difficultyIncrementRate))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a settlement is interesting to broadcast to clients.
|
||||
*
|
||||
* Do this by checking if the settlement is beyond a certain BCH threshold or a % movement threshold.
|
||||
*/
|
||||
isInterestingSettlement(contract, settlement) {
|
||||
const settlementSummary = this.getContractOutcome(contract, settlement);
|
||||
|
||||
// Check if the settlement is beyond the percentage threshold (only if it wasn't liquidated)
|
||||
const isBeyondPercentageThreshold = this.isBeyondPercentageThreshold(settlementSummary);
|
||||
if (isBeyondPercentageThreshold && !settlementSummary.liquidated) {
|
||||
this.incrementDifficulty();
|
||||
return {
|
||||
isInteresting: true,
|
||||
reason: 'Taker had a percentage change that exceeds the threshold',
|
||||
summary: settlementSummary,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the settlement is beyond the payout threshold
|
||||
const isBeyondPayoutThreshold = this.isBeyondPayoutThreshold(settlementSummary);
|
||||
if (isBeyondPayoutThreshold) {
|
||||
this.incrementDifficulty();
|
||||
return {
|
||||
isInteresting: true,
|
||||
reason: 'Taker payout was beyond the BCH threshold',
|
||||
summary: settlementSummary,
|
||||
};
|
||||
}
|
||||
|
||||
// If the checks all returned false, this wasn't an interesting settlement
|
||||
// Return isInteresting: false and undefined for the reason
|
||||
return {
|
||||
isInteresting: false,
|
||||
reason: undefined,
|
||||
summary: settlementSummary,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the contract payout is beyond a certain BCH threshold.
|
||||
* This threshold is dynamically adjusted based on the difficulty adjustment exponent.
|
||||
*
|
||||
* @param {SettlementSummary} settlementSummary
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBeyondPayoutThreshold(settlementSummary) {
|
||||
// Convert the difficulty filter exponent to a number
|
||||
const exponent = Number(this.difficultyFilterExponent);
|
||||
|
||||
// Calculate the exponential power as (exponent ^ exponent)
|
||||
const exponentialPower = Math.pow(exponent, exponent);
|
||||
|
||||
// Calculate the dynamic threshold as (base threshold * exponential power)
|
||||
const dynamicThreshold = Number(this.baseLiquidationThresholdSats) * exponentialPower;
|
||||
|
||||
// Get the sats difference between the input and payout
|
||||
const satsDifference = settlementSummary.outputSats - settlementSummary.inputSats;
|
||||
|
||||
// Check if the payout sats is greater than or equal to the dynamic threshold
|
||||
const isBeyondThreshold = Math.abs(Number(satsDifference)) >= dynamicThreshold;
|
||||
|
||||
return isBeyondThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Settled Contract Payout resulted in a % movement greater than a certain threshold.
|
||||
* This threshold is dynamically adjusted based on the difficulty adjustment exponent.
|
||||
*
|
||||
* @param {SettlementSummary} settlementSummary
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBeyondPercentageThreshold(settlementSummary) {
|
||||
// Convert the difficulty filter exponent to a number
|
||||
const exponent = Number(this.difficultyFilterExponent);
|
||||
|
||||
const exponentialPower = Math.pow(exponent, exponent);
|
||||
|
||||
// Calculate the dynamic threshold based on the difficulty adjustment exponent
|
||||
const dynamicThreshold = Number(this.basePercentageThreshold) * exponentialPower;
|
||||
|
||||
// Return a boolean indicating if the percentage change is greater than or equal to the dynamic threshold
|
||||
return Number(settlementSummary.percentageChange) >= dynamicThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of the contract params being pulled out and compared against a static value.
|
||||
*
|
||||
* @param {ContractData} contractData
|
||||
* @param {ContractAutomatedPayoutV2} automatedPayoutData
|
||||
* @returns {SettlementSummary}
|
||||
*/
|
||||
getContractOutcome(
|
||||
contractData,
|
||||
automatedPayoutData
|
||||
) {
|
||||
/**
|
||||
* Extract basic values that we can use to create a summary of the settlement.
|
||||
*/
|
||||
const takerSide = contractData.metadata.takerSide;
|
||||
|
||||
/** @type {bigint} */
|
||||
const takerInputSats = takerSide === 'short' ? contractData.metadata.shortInputInSatoshis : contractData.metadata.longInputInSatoshis;
|
||||
|
||||
/** @type {bigint} */
|
||||
const takerPayoutSats = takerSide === 'short' ? automatedPayoutData.shortPayoutInSatoshis : automatedPayoutData.longPayoutInSatoshis;
|
||||
|
||||
/** @type {bigint} */
|
||||
const maturityTimestamp = contractData.parameters.maturityTimestamp;
|
||||
|
||||
/** @type {bigint} */
|
||||
const settlementTimestamp = automatedPayoutData.settlementTimestamp;
|
||||
|
||||
/**
|
||||
* Derive values to create a summary of the settlement.
|
||||
*/
|
||||
// Get the difference between the taker input sats and payout sats
|
||||
const difference = takerInputSats - takerPayoutSats;
|
||||
|
||||
// Use a multiplier to preserve precision during bigint division
|
||||
const differenceMultiplier = 1000n;
|
||||
|
||||
// Multiply the difference by the multiplier
|
||||
const differenceMultipliedForDivision = difference * differenceMultiplier;
|
||||
|
||||
// Get the scaled percentage of the difference. We will divide by the difference multiplier to get the actual percentage.
|
||||
const scaledPercentageDifference = Math.abs(Number((differenceMultipliedForDivision / takerInputSats))) * 100;
|
||||
|
||||
// Divide the scaled percentage difference by the difference multiplier
|
||||
const percentageDifference = scaledPercentageDifference / Number(differenceMultiplier);
|
||||
|
||||
// Determine if the taker was liquidated by checking if they are getting any payout
|
||||
const takerLiquidated = takerPayoutSats === 0n;
|
||||
|
||||
// Calculate the actual duration in seconds by subtracting the settlement timestamp from the maturity timestamp
|
||||
const actualDurationMilliseconds = Number(settlementTimestamp - maturityTimestamp);
|
||||
|
||||
// Divide the actual duration milliseconds by the milliseconds per second
|
||||
const actualDurationInSeconds = actualDurationMilliseconds / 1000;
|
||||
|
||||
// Return the settlement summary
|
||||
return {
|
||||
liquidated: takerLiquidated,
|
||||
inputSats: takerInputSats,
|
||||
outputSats: takerPayoutSats,
|
||||
percentageChange: percentageDifference,
|
||||
oraclePublicKey: contractData.parameters.oraclePublicKey,
|
||||
settlementTimestamp: actualDurationInSeconds,
|
||||
durationInSeconds: contractData.parameters.durationInSeconds,
|
||||
takerLeverage: contractData.parameters.takerLeverage,
|
||||
makerLeverage: contractData.parameters.makerLeverage,
|
||||
};
|
||||
}
|
||||
}
|
||||
152
src/generate-examples.ts
Normal file
152
src/generate-examples.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Generate randomized examples from a single seed contract.
|
||||
*
|
||||
* Model:
|
||||
* - Create N copies.
|
||||
* - Set random input sats in [5_000_000, 1_000_000_000].
|
||||
* - Set random percentage change in [-100, +100].
|
||||
* - Set output sats directly from that percentage.
|
||||
* - Increment settlement timestamp by random 3-10 minutes.
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
import { contractData, automatedPayoutData } from './example.js';
|
||||
import { encodeExtendedJson } from './ext-json.js';
|
||||
|
||||
export type Example = {
|
||||
contractData: typeof contractData;
|
||||
automatedPayoutData: typeof automatedPayoutData;
|
||||
};
|
||||
|
||||
const EXAMPLE_COUNT = Number(process.env.EXAMPLE_COUNT ?? 100);
|
||||
const MIN_INPUT_SATS = 5_000_000n;
|
||||
const MAX_INPUT_SATS = 1_000_000_000n;
|
||||
const MIN_PERCENT_CHANGE = -100;
|
||||
const MAX_PERCENT_CHANGE = 100;
|
||||
const MIN_TIMESTAMP_INCREMENT_SECONDS = 3 * 60;
|
||||
const MAX_TIMESTAMP_INCREMENT_SECONDS = 60 * 60;
|
||||
|
||||
/**
|
||||
* Deep clone an object while preserving BigInt values.
|
||||
*/
|
||||
function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(
|
||||
JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? `${value.toString()}n` : value)),
|
||||
(_, value) => {
|
||||
if (typeof value === 'string' && /^\d+n$/.test(value)) {
|
||||
return BigInt(value.slice(0, -1));
|
||||
}
|
||||
return value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random integer in [min, max].
|
||||
*/
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random bigint in [min, max].
|
||||
*/
|
||||
function randomBigIntInRange(min: bigint, max: bigint): bigint {
|
||||
return BigInt(randomInt(Number(min), Number(max)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next settlement timestamp by appending 3-10 minutes.
|
||||
*/
|
||||
function getNextSettlementTimestamp(previousSettlementTimestampSeconds: number): number {
|
||||
return previousSettlementTimestampSeconds + randomInt(
|
||||
MIN_TIMESTAMP_INCREMENT_SECONDS,
|
||||
MAX_TIMESTAMP_INCREMENT_SECONDS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a random percentage with one decimal place in [-100, +100].
|
||||
*/
|
||||
function randomPercentageChange(): number {
|
||||
const scaled = randomInt(MIN_PERCENT_CHANGE * 10, MAX_PERCENT_CHANGE * 10);
|
||||
return scaled / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute taker payout from taker input and a signed percentage.
|
||||
* -100 => 0x input
|
||||
* 0 => 1x input
|
||||
* +100 => 2x input
|
||||
*/
|
||||
function computeTakerOutputFromPercent(takerInputSats: bigint, percentChange: number): bigint {
|
||||
const multiplier = 1 + percentChange / 100;
|
||||
return BigInt(Math.max(0, Math.round(Number(takerInputSats) * multiplier)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply one random input amount to both sides.
|
||||
*
|
||||
* This keeps funding balanced and supports +/-100% outcomes sensibly.
|
||||
*/
|
||||
function applyRandomInputs(contract: typeof contractData): bigint {
|
||||
const randomInput = randomBigIntInRange(MIN_INPUT_SATS, MAX_INPUT_SATS);
|
||||
|
||||
contract.metadata.shortInputInSatoshis = randomInput;
|
||||
contract.metadata.longInputInSatoshis = randomInput;
|
||||
|
||||
const startPrice = Number(contract.metadata.startPrice);
|
||||
contract.metadata.shortInputInOracleUnits = Number(randomInput) / startPrice;
|
||||
contract.metadata.longInputInOracleUnits = Number(randomInput) / startPrice;
|
||||
|
||||
const totalInputs = randomInput + randomInput;
|
||||
contract.parameters.payoutSats = totalInputs;
|
||||
contract.metadata.nominalUnits = Number(totalInputs) / startPrice;
|
||||
contract.parameters.nominalUnitsXSatsPerBch = BigInt(
|
||||
Math.floor(contract.metadata.nominalUnits * 100_000_000),
|
||||
);
|
||||
|
||||
return randomInput;
|
||||
}
|
||||
|
||||
const examples: Example[] = [];
|
||||
let latestSettlementTimestampSeconds = Number(contractData.parameters.maturityTimestamp);
|
||||
|
||||
for (let i = 0; i < EXAMPLE_COUNT; i++) {
|
||||
const contract = deepClone(contractData);
|
||||
const payout = deepClone(automatedPayoutData);
|
||||
|
||||
// 1) Randomize inputs.
|
||||
const takerInputSats = applyRandomInputs(contract);
|
||||
|
||||
// 2) Randomize percentage change and derive taker output sats.
|
||||
const randomPercentChange = randomPercentageChange();
|
||||
const takerOutputSats = computeTakerOutputFromPercent(takerInputSats, randomPercentChange);
|
||||
|
||||
// 3) Set payout outputs, bounded by total available contract payout.
|
||||
const totalPayoutSats = contract.metadata.shortInputInSatoshis
|
||||
+ contract.metadata.longInputInSatoshis
|
||||
- contract.metadata.minerCostInSatoshis;
|
||||
const boundedTakerOutputSats = takerOutputSats > totalPayoutSats ? totalPayoutSats : takerOutputSats;
|
||||
|
||||
if (contract.metadata.takerSide === 'short') {
|
||||
payout.shortPayoutInSatoshis = boundedTakerOutputSats;
|
||||
payout.longPayoutInSatoshis = totalPayoutSats - boundedTakerOutputSats;
|
||||
} else {
|
||||
payout.longPayoutInSatoshis = boundedTakerOutputSats;
|
||||
payout.shortPayoutInSatoshis = totalPayoutSats - boundedTakerOutputSats;
|
||||
}
|
||||
|
||||
// 4) Append settlement timestamps by 3-10 minute increments.
|
||||
latestSettlementTimestampSeconds = getNextSettlementTimestamp(latestSettlementTimestampSeconds);
|
||||
payout.settlementTimestamp = BigInt(latestSettlementTimestampSeconds);
|
||||
|
||||
examples.push({
|
||||
contractData: contract,
|
||||
automatedPayoutData: payout,
|
||||
});
|
||||
}
|
||||
|
||||
writeFileSync('examples.json', encodeExtendedJson(examples));
|
||||
console.log(`Generated ${examples.length} examples and saved to examples.json`);
|
||||
16
src/generate-html.ts
Normal file
16
src/generate-html.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Generate the HTML file by replacing the serializedRows placeholder in the template.
|
||||
*/
|
||||
export function generateHtml(difficultySeries: string): string {
|
||||
// Get the directory of the current file.
|
||||
const pwd = path.dirname(import.meta.filename);
|
||||
|
||||
// Read the template file.
|
||||
const template = readFileSync(path.join(pwd, 'html-template.html'), 'utf8');
|
||||
|
||||
// Replace the serializedRows placeholder in the template.
|
||||
return template.replace('{{ serializedRows }}', difficultySeries);
|
||||
}
|
||||
223
src/html-template.html
Normal file
223
src/html-template.html
Normal file
@@ -0,0 +1,223 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Difficulty Threshold Timeline</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin: 0; font-family: sans-serif; background: #111; color: #ddd; }
|
||||
.container { padding: 16px; position: relative; }
|
||||
h1 { margin: 0 0 8px; font-size: 20px; }
|
||||
h2 { margin: 20px 0 8px; font-size: 16px; color: #ddd; }
|
||||
p { margin: 0 0 16px; color: #aaa; }
|
||||
svg { width: 100%; height: 520px; border: 1px solid #333; background: #171717; }
|
||||
.axis-label { fill: #bdbdbd; font-size: 12px; }
|
||||
.tick-label { fill: #9a9a9a; font-size: 11px; }
|
||||
.line-threshold { fill: none; stroke: #f5c542; stroke-width: 1.8; }
|
||||
.line-threshold-neg { fill: none; stroke: #f5c542; stroke-width: 1.8; stroke-dasharray: 4 3; }
|
||||
.line-metric { fill: none; stroke: #9ca3af; stroke-width: 1.2; opacity: 0.7; }
|
||||
.band-not-interesting { fill: rgba(245, 197, 66, 0.18); }
|
||||
.point-interesting { fill: #3b82f6; stroke: #dbeafe; stroke-width: 1; }
|
||||
.point-not-interesting { fill: #ef4444; stroke: #fee2e2; stroke-width: 1; }
|
||||
.grid { stroke: #2a2a2a; stroke-width: 1; }
|
||||
.zero-line { stroke: #767676; stroke-width: 1.5; stroke-dasharray: 3 3; }
|
||||
.legend { display: flex; gap: 12px; margin-top: 12px; color: #bbb; font-size: 12px; flex-wrap: wrap; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
||||
.dot-blue { background: #3b82f6; }
|
||||
.dot-red { background: #ef4444; }
|
||||
.dot-line { width: 14px; height: 3px; border-radius: 2px; background: #f5c542; margin-top: 4px; }
|
||||
.hover-hit { fill: transparent; cursor: pointer; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
min-width: 220px;
|
||||
max-width: 340px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #3d3d3d;
|
||||
border-radius: 6px;
|
||||
background: rgba(12, 12, 12, 0.95);
|
||||
color: #eee;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: 3;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Difficulty Threshold Timelines</h1>
|
||||
<p>Each chart is centered around 0. The soft yellow band between +threshold and -threshold is the non-interesting range.</p>
|
||||
|
||||
<h2>BCH Value Threshold</h2>
|
||||
<svg id="bch-chart" viewBox="0 0 1200 520" role="img" aria-label="BCH threshold chart"></svg>
|
||||
|
||||
<h2>% Change Threshold</h2>
|
||||
<svg id="percent-chart" viewBox="0 0 1200 520" role="img" aria-label="Percentage threshold chart"></svg>
|
||||
|
||||
<div class="legend">
|
||||
<span><span class="dot dot-line"></span>+ Threshold</span>
|
||||
<span><span class="dot dot-line"></span>- Threshold (dashed)</span>
|
||||
<span><span class="dot dot-blue"></span>Interesting settlement</span>
|
||||
<span><span class="dot dot-red"></span>Not interesting settlement</span>
|
||||
</div>
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
</div>
|
||||
<script>
|
||||
const rows = {{ serializedRows }};
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
const formatNumber = (value) => {
|
||||
const abs = Math.abs(value);
|
||||
if (abs >= 1_000_000 || (abs > 0 && abs < 0.001)) return value.toExponential(2);
|
||||
return value.toFixed(4);
|
||||
};
|
||||
|
||||
const renderThresholdChart = (svg, config) => {
|
||||
const width = 1200;
|
||||
const height = 520;
|
||||
const margin = { top: 20, right: 24, bottom: 56, left: 100 };
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
const xValues = rows.map((r) => r.settlementTimestamp);
|
||||
const minX = Math.min(...xValues);
|
||||
const maxX = Math.max(...xValues);
|
||||
const minY = config.minY;
|
||||
const maxY = config.maxY;
|
||||
const clampY = (value) => Math.min(maxY, Math.max(minY, value));
|
||||
|
||||
const xScale = (value) => {
|
||||
if (maxX === minX) return margin.left + innerWidth / 2;
|
||||
return margin.left + ((value - minX) / (maxX - minX)) * innerWidth;
|
||||
};
|
||||
const yScale = (value) => margin.top + innerHeight - ((value - minY) / (maxY - minY)) * innerHeight;
|
||||
|
||||
const xTicks = 6;
|
||||
const yTicks = 6;
|
||||
let svgParts = [];
|
||||
|
||||
for (let i = 0; i <= yTicks; i++) {
|
||||
const value = minY + ((maxY - minY) * i) / yTicks;
|
||||
const y = yScale(value);
|
||||
svgParts.push('<line class="grid" x1="' + margin.left + '" y1="' + y + '" x2="' + (margin.left + innerWidth) + '" y2="' + y + '" />');
|
||||
svgParts.push('<text class="tick-label" x="' + (margin.left - 8) + '" y="' + (y + 4) + '" text-anchor="end">' + formatNumber(value) + '</text>');
|
||||
}
|
||||
|
||||
for (let i = 0; i <= xTicks; i++) {
|
||||
const value = minX + ((maxX - minX) * i) / xTicks;
|
||||
const x = xScale(value);
|
||||
const date = new Date(value * 1000).toISOString().slice(11, 19);
|
||||
svgParts.push('<line class="grid" x1="' + x + '" y1="' + margin.top + '" x2="' + x + '" y2="' + (margin.top + innerHeight) + '" />');
|
||||
svgParts.push('<text class="tick-label" x="' + x + '" y="' + (margin.top + innerHeight + 18) + '" text-anchor="middle">' + date + '</text>');
|
||||
}
|
||||
|
||||
svgParts.push('<line x1="' + margin.left + '" y1="' + (margin.top + innerHeight) + '" x2="' + (margin.left + innerWidth) + '" y2="' + (margin.top + innerHeight) + '" stroke="#999" />');
|
||||
svgParts.push('<line x1="' + margin.left + '" y1="' + margin.top + '" x2="' + margin.left + '" y2="' + (margin.top + innerHeight) + '" stroke="#999" />');
|
||||
svgParts.push('<text class="axis-label" x="' + (margin.left + innerWidth / 2) + '" y="' + (height - 16) + '" text-anchor="middle">Time</text>');
|
||||
svgParts.push('<text class="axis-label" x="22" y="' + (margin.top + innerHeight / 2) + '" text-anchor="middle" transform="rotate(-90 22 ' + (margin.top + innerHeight / 2) + ')">' + config.yAxisLabel + '</text>');
|
||||
|
||||
// Shade the non-interesting band between +threshold and -threshold.
|
||||
let topPath = '';
|
||||
let bottomPath = '';
|
||||
rows.forEach((row, index) => {
|
||||
const x = xScale(row.settlementTimestamp);
|
||||
const threshold = clampY(config.thresholdValue(row));
|
||||
const topY = yScale(threshold);
|
||||
const bottomY = yScale(-threshold);
|
||||
topPath += (index === 0 ? 'M' : 'L') + x + ' ' + topY + ' ';
|
||||
bottomPath = 'L' + x + ' ' + bottomY + ' ' + bottomPath;
|
||||
});
|
||||
svgParts.push('<path class="band-not-interesting" d="' + topPath + bottomPath + 'Z" />');
|
||||
|
||||
const zeroY = yScale(0);
|
||||
svgParts.push('<line class="zero-line" x1="' + margin.left + '" y1="' + zeroY + '" x2="' + (margin.left + innerWidth) + '" y2="' + zeroY + '" />');
|
||||
|
||||
let positiveThresholdPath = '';
|
||||
let negativeThresholdPath = '';
|
||||
let metricPath = '';
|
||||
rows.forEach((row, index) => {
|
||||
const x = xScale(row.settlementTimestamp);
|
||||
const threshold = clampY(config.thresholdValue(row));
|
||||
positiveThresholdPath += (index === 0 ? 'M' : 'L') + x + ' ' + yScale(threshold) + ' ';
|
||||
negativeThresholdPath += (index === 0 ? 'M' : 'L') + x + ' ' + yScale(-threshold) + ' ';
|
||||
metricPath += (index === 0 ? 'M' : 'L') + x + ' ' + yScale(clampY(config.metricValue(row))) + ' ';
|
||||
});
|
||||
svgParts.push('<path class="line-threshold" d="' + positiveThresholdPath + '" />');
|
||||
svgParts.push('<path class="line-threshold-neg" d="' + negativeThresholdPath + '" />');
|
||||
svgParts.push('<path class="line-metric" d="' + metricPath + '" />');
|
||||
|
||||
rows.forEach((row) => {
|
||||
const x = xScale(row.settlementTimestamp);
|
||||
const metric = config.metricValue(row);
|
||||
const threshold = config.thresholdValue(row);
|
||||
const y = yScale(clampY(metric));
|
||||
const cssClass = row.isInteresting ? 'point-interesting' : 'point-not-interesting';
|
||||
const tooltipText = [
|
||||
config.title,
|
||||
'Time: ' + row.timeIso,
|
||||
'Interesting: ' + row.isInteresting,
|
||||
'Reason: ' + (row.reason || 'N/A'),
|
||||
config.metricLabel + ': ' + formatNumber(metric),
|
||||
config.thresholdLabel + ': ' + formatNumber(threshold),
|
||||
'Input BCH: ' + formatNumber(row.inputBch),
|
||||
'Payout Sats: ' + row.outputSats,
|
||||
].join('\\n');
|
||||
const escapedTooltipText = tooltipText
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
svgParts.push('<circle class="' + cssClass + '" cx="' + x + '" cy="' + y + '" r="4.5"></circle>');
|
||||
svgParts.push('<circle class="hover-hit" data-tooltip="' + escapedTooltipText + '" cx="' + x + '" cy="' + y + '" r="10"></circle>');
|
||||
});
|
||||
|
||||
svg.innerHTML = svgParts.join('');
|
||||
|
||||
const container = document.querySelector('.container');
|
||||
const hoverPoints = svg.querySelectorAll('.hover-hit');
|
||||
hoverPoints.forEach((point) => {
|
||||
point.addEventListener('mousemove', (event) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const tooltipText = event.target.getAttribute('data-tooltip') || '';
|
||||
tooltip.textContent = tooltipText;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.left = (event.clientX - rect.left + 14) + 'px';
|
||||
tooltip.style.top = (event.clientY - rect.top + 14) + 'px';
|
||||
});
|
||||
point.addEventListener('mouseleave', () => {
|
||||
tooltip.style.display = 'none';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!rows.length) {
|
||||
document.getElementById('bch-chart').innerHTML = '<text x="24" y="40" class="axis-label">No data available.</text>';
|
||||
document.getElementById('percent-chart').innerHTML = '<text x="24" y="40" class="axis-label">No data available.</text>';
|
||||
} else {
|
||||
renderThresholdChart(document.getElementById('bch-chart'), {
|
||||
title: 'BCH Value Perspective',
|
||||
yAxisLabel: 'BCH Delta',
|
||||
metricLabel: 'Actual BCH Delta',
|
||||
thresholdLabel: 'BCH Threshold',
|
||||
metricValue: (row) => row.bchDelta,
|
||||
thresholdValue: (row) => row.bchThreshold,
|
||||
minY: -20,
|
||||
maxY: 20,
|
||||
});
|
||||
|
||||
renderThresholdChart(document.getElementById('percent-chart'), {
|
||||
title: '% Change Perspective',
|
||||
yAxisLabel: 'Percentage Change',
|
||||
metricLabel: 'Actual % Change',
|
||||
thresholdLabel: '% Threshold',
|
||||
metricValue: (row) => row.signedPercentageChange,
|
||||
thresholdValue: (row) => row.percentageThreshold,
|
||||
minY: -100,
|
||||
maxY: 100,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
172
src/index.ts
Normal file
172
src/index.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import type { Example } from './generate-examples.js';
|
||||
|
||||
import { InterestingSettlementFilter } from './filter.js';
|
||||
|
||||
import { decodeExtendedJson } from './ext-json.js';
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
import { generateHtml } from './generate-html.js';
|
||||
|
||||
export interface SettlementSummary {
|
||||
liquidated: boolean;
|
||||
inputSats: bigint;
|
||||
outputSats: bigint;
|
||||
durationInSeconds: number;
|
||||
takerLeverage: number;
|
||||
makerLeverage: number;
|
||||
percentageChange: number;
|
||||
oraclePublicKey: string;
|
||||
settlementTimestamp: number;
|
||||
}
|
||||
|
||||
export interface InterestingContractResult {
|
||||
isInteresting: boolean;
|
||||
reason?: string;
|
||||
summary: SettlementSummary;
|
||||
}
|
||||
|
||||
interface DifficultySeriesRow {
|
||||
index: number;
|
||||
timeIso: string;
|
||||
settlementTimestamp: number;
|
||||
contractTimestamp: number;
|
||||
elapsedSeconds: number;
|
||||
isInteresting: boolean;
|
||||
reason: string;
|
||||
liquidated: boolean;
|
||||
inputSats: number;
|
||||
inputBch: number;
|
||||
outputSats: number;
|
||||
outputSatsBch: number;
|
||||
percentageChange: number;
|
||||
signedPercentageChange: number;
|
||||
bchDelta: number;
|
||||
bchThreshold: number;
|
||||
percentageThreshold: number;
|
||||
}
|
||||
|
||||
// Read the examples file.
|
||||
const examplesJson = readFileSync('examples.json', 'utf8');
|
||||
|
||||
// Parse the extended JSON into objects.
|
||||
const examples: Example[] = decodeExtendedJson(examplesJson);
|
||||
|
||||
// Initialize the filter
|
||||
const filter = new InterestingSettlementFilter({
|
||||
// Base values
|
||||
baseLiquidationThresholdSats: 100_000_000n,
|
||||
basePercentageThreshold: 20,
|
||||
|
||||
// Exponential values
|
||||
difficultyFilterExponent: 1.0,
|
||||
minDifficultyFilterExponent: 0.4,
|
||||
maxDifficultyFilterExponent: 5,
|
||||
difficultyIncrementRate: 0.1,
|
||||
|
||||
// Interval
|
||||
difficultyAdjustmentInterval: 1000 * 60 * 30,
|
||||
});
|
||||
|
||||
// Start the filter, this just starts the invterval timer
|
||||
filter.start();
|
||||
|
||||
// Initialize an array to store the interesting examples and the difficulty series.
|
||||
const interestingExamples: {
|
||||
example: Example;
|
||||
result: InterestingContractResult;
|
||||
}[] = [];
|
||||
|
||||
// Initialize an array to store the difficulty series.
|
||||
const difficultySeries: DifficultySeriesRow[] = [];
|
||||
|
||||
// Initialize a function to get the contract timestamp in seconds.
|
||||
const getContractTimestampSeconds = (example: Example): number => Number(example.contractData.parameters.startTimestamp);
|
||||
|
||||
// Initialize a function to get the settlement timestamp in seconds.
|
||||
const getSettlementTimestampSeconds = (example: Example): number => {
|
||||
const settlementTimestamp = example.automatedPayoutData.settlementTimestamp;
|
||||
return settlementTimestamp !== undefined
|
||||
? Number(settlementTimestamp)
|
||||
: Number(example.contractData.parameters.maturityTimestamp);
|
||||
};
|
||||
|
||||
// Initialize a variable to store the previous settlement timestamp so we can calculate the decremental steps.
|
||||
let previousSettlementTimestamp = getSettlementTimestampSeconds(examples.at(0)!);
|
||||
|
||||
// Loop through the examples.
|
||||
for (const [index, example] of examples.entries()) {
|
||||
// Get the contract and settlement timestamps
|
||||
const contractTimestamp = getContractTimestampSeconds(example);
|
||||
const settlementTimestamp = getSettlementTimestampSeconds(example);
|
||||
const elapsedSeconds = Math.max(0, settlementTimestamp - contractTimestamp);
|
||||
|
||||
// Calculate the decremental steps to adjust the difficulty.
|
||||
const stepCount = Math.floor((getSettlementTimestampSeconds(example) - previousSettlementTimestamp) / (filter.difficultyAdjustmentInterval / 1000));
|
||||
|
||||
// Update the previous settlement timestamp.
|
||||
if (stepCount > 0) {
|
||||
previousSettlementTimestamp = settlementTimestamp;
|
||||
}
|
||||
|
||||
// Decrement the difficulty.
|
||||
filter.decrementDifficulty(stepCount);
|
||||
|
||||
// Capture thresholds before this settlement mutates difficulty.
|
||||
const bchThresholdSats = Number(filter.baseLiquidationThresholdSats) * Math.pow(filter.difficultyFilterExponent, filter.difficultyFilterExponent);
|
||||
const bchThreshold = bchThresholdSats / 100_000_000;
|
||||
const percentageThreshold = Number(filter.basePercentageThreshold) * Math.pow(filter.difficultyFilterExponent, filter.difficultyFilterExponent);
|
||||
|
||||
// Run the calculation and grab the results
|
||||
const result = filter.isInterestingSettlement(example.contractData, example.automatedPayoutData);
|
||||
const summary = result.summary;
|
||||
|
||||
// Calculate the input and output sats.
|
||||
const inputSats = Number(summary.inputSats);
|
||||
const outputSats = Number(summary.outputSats);
|
||||
const inputBch = inputSats / 100_000_000;
|
||||
const outputSatsBch = outputSats / 100_000_000;
|
||||
const bchDelta = outputSatsBch - inputBch;
|
||||
|
||||
// Calculate the signed percentage change.
|
||||
const signedPercentageChange = ((outputSats - inputSats) / inputSats) * 100;
|
||||
|
||||
// Add the row to the difficulty series.
|
||||
difficultySeries.push({
|
||||
index,
|
||||
timeIso: new Date(settlementTimestamp * 1000).toISOString(),
|
||||
settlementTimestamp,
|
||||
contractTimestamp,
|
||||
elapsedSeconds,
|
||||
isInteresting: result.isInteresting,
|
||||
reason: result.reason ?? '',
|
||||
liquidated: summary.liquidated,
|
||||
inputSats,
|
||||
inputBch,
|
||||
outputSats,
|
||||
outputSatsBch,
|
||||
percentageChange: Number(summary.percentageChange),
|
||||
signedPercentageChange,
|
||||
bchDelta,
|
||||
bchThreshold,
|
||||
percentageThreshold,
|
||||
});
|
||||
|
||||
if (result.isInteresting) {
|
||||
interestingExamples.push({ example, result: result as InterestingContractResult });
|
||||
}
|
||||
}
|
||||
|
||||
// Write the difficulty series to file.
|
||||
writeFileSync('difficulty-series.json', JSON.stringify(difficultySeries, null, 2));
|
||||
|
||||
// Generate the HTML file and write it to the file system.
|
||||
writeFileSync('difficulty-chart.html', generateHtml(JSON.stringify(difficultySeries)));
|
||||
|
||||
// Log the interesting examples.
|
||||
console.log(interestingExamples.map(example => example.result.reason));
|
||||
console.log(`Found ${interestingExamples.length} interesting examples out of ${examples.length}`);
|
||||
console.log('Wrote difficulty-series.json, difficulty-series.csv, difficulty-chart.html');
|
||||
|
||||
// Stop the filter
|
||||
filter.stop();
|
||||
44
tsconfig.json
Normal file
44
tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": ["node"],
|
||||
// For nodejs:
|
||||
"lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user