commit d25ed7cb9debcfe710e97408e125bc8b93fad985 Author: Harvmaster Date: Mon Feb 23 09:49:45 2026 +0000 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..245e8b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules +difficulty-chart.html +difficulty-series.json +examples.json \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..11bb3b2 --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..982a331 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/example.ts b/src/example.ts new file mode 100644 index 0000000..b9efa98 --- /dev/null +++ b/src/example.ts @@ -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 +} diff --git a/src/ext-json.ts b/src/ext-json.ts new file mode 100644 index 0000000..7170460 --- /dev/null +++ b/src/ext-json.ts @@ -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 ``; + } else if (value instanceof Uint8Array) { + return ``; + } + + return value; +}; + +export const extendedJsonReviver = function (value: any): any { + // Define RegEx that matches our Extended JSON fields. + const bigIntRegex = /^[+-]?[0-9]*)n>$/; + const uint8ArrayRegex = /^[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; +}; diff --git a/src/filter.d.ts b/src/filter.d.ts new file mode 100644 index 0000000..c3be23f --- /dev/null +++ b/src/filter.d.ts @@ -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, + automatedPayoutData: Record, + ): SettlementSummary; + public isInterestingSettlement( + contractData: Record, + automatedPayoutData: Record, + ): InterestingContractResult; +} diff --git a/src/filter.js b/src/filter.js new file mode 100644 index 0000000..7cde8ec --- /dev/null +++ b/src/filter.js @@ -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, + }; + } +} \ No newline at end of file diff --git a/src/generate-examples.ts b/src/generate-examples.ts new file mode 100644 index 0000000..f21ae6a --- /dev/null +++ b/src/generate-examples.ts @@ -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(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`); \ No newline at end of file diff --git a/src/generate-html.ts b/src/generate-html.ts new file mode 100644 index 0000000..fde1142 --- /dev/null +++ b/src/generate-html.ts @@ -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); +} diff --git a/src/html-template.html b/src/html-template.html new file mode 100644 index 0000000..02d9f66 --- /dev/null +++ b/src/html-template.html @@ -0,0 +1,223 @@ + + + + + + Difficulty Threshold Timeline + + + +
+

Difficulty Threshold Timelines

+

Each chart is centered around 0. The soft yellow band between +threshold and -threshold is the non-interesting range.

+ +

BCH Value Threshold

+ + +

% Change Threshold

+ + +
+ + Threshold + - Threshold (dashed) + Interesting settlement + Not interesting settlement +
+
+
+ + + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4cb3a55 --- /dev/null +++ b/src/index.ts @@ -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(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cab4a75 --- /dev/null +++ b/tsconfig.json @@ -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, + } +}