From 394ab732768b4844e0abddff7a4bc719267b87ef Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 26 Nov 2025 14:30:41 -0800 Subject: [PATCH] feat(encryption): Adding encryption REST API route --- package-lock.json | 888 +----------------- package.json | 2 +- .../rest-api/encryption/controller.js | 100 ++ src/controllers/rest-api/encryption/router.js | 51 + src/controllers/rest-api/index.js | 4 + src/use-cases/encryption-use-cases.js | 120 +++ src/use-cases/index.js | 7 + .../controllers/encryption-controller-unit.js | 203 ++++ test/unit/controllers/rest-api-index-unit.js | 7 + .../use-cases/encryption-use-cases-unit.js | 247 +++++ 10 files changed, 774 insertions(+), 855 deletions(-) create mode 100644 src/controllers/rest-api/encryption/controller.js create mode 100644 src/controllers/rest-api/encryption/router.js create mode 100644 src/use-cases/encryption-use-cases.js create mode 100644 test/unit/controllers/encryption-controller-unit.js create mode 100644 test/unit/use-cases/encryption-use-cases-unit.js diff --git a/package-lock.json b/package-lock.json index 27a7261..94dd53d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "cors": "2.8.5", "dotenv": "16.3.1", "express": "5.1.0", - "minimal-slp-wallet": "5.13.3", + "minimal-slp-wallet": "/home/trout/work/psf/code/api-dev/minimal-slp-wallet", "psffpp": "1.2.0", "slp-token-media": "1.2.10", "winston": "3.11.0", @@ -75,6 +75,37 @@ "standard": "17.1.2" } }, + "../minimal-slp-wallet": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue": "1.0.11", + "@psf/bch-js": "/home/trout/work/psf/code/api-dev/bch-js", + "bch-consumer": "1.6.2", + "bch-donation": "1.1.2", + "crypto-js": "4.0.0" + }, + "devDependencies": { + "apidoc": "1.2.0", + "c8": "10.1.3", + "chai": "4.2.0", + "coveralls": "3.1.0", + "esbuild": "0.20.0", + "esbuild-plugin-polyfill-node": "^0.3.0", + "eslint": "7.17.0", + "eslint-config-prettier": "7.1.0", + "eslint-config-standard": "16.0.2", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "3.3.1", + "eslint-plugin-standard": "4.0.1", + "husky": "4.3.8", + "lodash.clonedeep": "4.5.0", + "mocha": "9.2.1", + "semantic-release": "19.0.3", + "sinon": "9.2.0", + "standard": "16.0.4" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -1685,539 +1716,6 @@ "node": ">=4.5.0" } }, - "node_modules/bch-consumer": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/bch-consumer/-/bch-consumer-1.6.2.tgz", - "integrity": "sha512-PclXVzmWtwqxe2ba6Mo9QbqxJ+5rUog8c+Ro5PV/W3IxkKNotVRa71bLei99AIELDO3A3ljfXkoCNsSM7tR37A==", - "license": "MIT", - "dependencies": { - "@psf/bch-js": "6.7.3", - "apidoc": "0.51.0", - "axios": "0.25.0" - } - }, - "node_modules/bch-consumer/node_modules/@psf/bch-js": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-6.7.3.tgz", - "integrity": "sha512-z6oJvPAXxSObPhqUAxCkCuPL2bp3Bs1oyhBZpQ5SR6cbJzcOMhuI4bHho2srH5agSCU+bbjehBMRGvo18eGW0A==", - "license": "MIT", - "dependencies": { - "@chris.troutner/bip32-utils": "1.0.5", - "@psf/bip21": "2.0.1", - "@psf/bitcoincash-ops": "2.0.0", - "@psf/bitcoincashjs-lib": "4.0.3", - "@psf/coininfo": "4.0.0", - "axios": "0.26.1", - "bc-bip68": "1.0.5", - "bchaddrjs-slp": "0.2.5", - "bigi": "1.4.2", - "bignumber.js": "9.0.0", - "bip-schnorr": "0.3.0", - "bip38": "2.0.2", - "bip39": "3.0.2", - "bip66": "1.1.5", - "bitcoinjs-message": "2.0.0", - "bs58": "4.0.1", - "ecashaddrjs": "1.0.7", - "ini": "1.3.8", - "randombytes": "2.0.6", - "safe-buffer": "5.1.2", - "satoshi-bitcoin": "1.0.4", - "slp-mdm": "0.0.6", - "slp-parser": "0.0.4", - "wif": "2.0.6" - } - }, - "node_modules/bch-consumer/node_modules/@psf/bch-js/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/bch-consumer/node_modules/apidoc": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.51.0.tgz", - "integrity": "sha512-3P4srhm6NA+kE/YRM4qL5jESElpQQJL+Z8n7hyr4uC+1DSwGzmE6adXPVxuT/hpDyShrqmRlHk8hf40RSqEsNw==", - "license": "MIT", - "os": [ - "darwin", - "freebsd", - "linux", - "openbsd", - "win32" - ], - "dependencies": { - "bootstrap": "3.4.1", - "commander": "^8.3.0", - "diff-match-patch": "^1.0.5", - "esbuild-loader": "^2.16.0", - "expose-loader": "^3.1.0", - "fs-extra": "^10.0.0", - "glob": "^7.2.0", - "handlebars": "^4.7.7", - "iconv-lite": "^0.6.3", - "jquery": "^3.6.0", - "klaw-sync": "^6.0.0", - "lodash": "^4.17.21", - "markdown-it": "^12.2.0", - "nodemon": "^2.0.15", - "path-to-regexp": "^6.2.0", - "prismjs": "^1.25.0", - "semver": "^7.3.5", - "style-loader": "^3.3.1", - "url-parse": "^1.5.3", - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "winston": "^3.3.3" - }, - "bin": { - "apidoc": "bin/apidoc" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/bch-consumer/node_modules/axios": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", - "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.7" - } - }, - "node_modules/bch-consumer/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/bch-consumer/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/bch-consumer/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/bch-consumer/node_modules/expose-loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", - "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/bch-consumer/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/bch-consumer/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bch-consumer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/bch-consumer/node_modules/nodemon": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", - "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^3.2.7", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/bch-consumer/node_modules/nodemon/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/bch-consumer/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "node_modules/bch-consumer/node_modules/randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/bch-consumer/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/bch-consumer/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/bch-consumer/node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "license": "MIT", - "dependencies": { - "semver": "~7.0.0" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/bch-consumer/node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/bch-consumer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/bch-donation": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/bch-donation/-/bch-donation-1.1.2.tgz", - "integrity": "sha512-V7xQ23M6Ocavb1Nj4/pATwaLX/uF/9s2nWir3f+F3aetD3kMmsRHFRuXJwGQG3dSC5fJtRR/M5Z2zynY3t91Xg==", - "license": "MIT", - "dependencies": { - "apidoc": "0.51.0" - } - }, - "node_modules/bch-donation/node_modules/apidoc": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.51.0.tgz", - "integrity": "sha512-3P4srhm6NA+kE/YRM4qL5jESElpQQJL+Z8n7hyr4uC+1DSwGzmE6adXPVxuT/hpDyShrqmRlHk8hf40RSqEsNw==", - "license": "MIT", - "os": [ - "darwin", - "freebsd", - "linux", - "openbsd", - "win32" - ], - "dependencies": { - "bootstrap": "3.4.1", - "commander": "^8.3.0", - "diff-match-patch": "^1.0.5", - "esbuild-loader": "^2.16.0", - "expose-loader": "^3.1.0", - "fs-extra": "^10.0.0", - "glob": "^7.2.0", - "handlebars": "^4.7.7", - "iconv-lite": "^0.6.3", - "jquery": "^3.6.0", - "klaw-sync": "^6.0.0", - "lodash": "^4.17.21", - "markdown-it": "^12.2.0", - "nodemon": "^2.0.15", - "path-to-regexp": "^6.2.0", - "prismjs": "^1.25.0", - "semver": "^7.3.5", - "style-loader": "^3.3.1", - "url-parse": "^1.5.3", - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "winston": "^3.3.3" - }, - "bin": { - "apidoc": "bin/apidoc" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/bch-donation/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/bch-donation/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/bch-donation/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/bch-donation/node_modules/expose-loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", - "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/bch-donation/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/bch-donation/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bch-donation/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/bch-donation/node_modules/nodemon": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", - "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^3.2.7", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/bch-donation/node_modules/nodemon/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/bch-donation/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "node_modules/bch-donation/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/bch-donation/node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "license": "MIT", - "dependencies": { - "semver": "~7.0.0" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/bch-donation/node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/bch-donation/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/bchaddrjs-slp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz", @@ -3038,12 +2536,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==", - "license": "MIT" - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -6283,298 +5775,8 @@ } }, "node_modules/minimal-slp-wallet": { - "version": "5.13.3", - "resolved": "https://registry.npmjs.org/minimal-slp-wallet/-/minimal-slp-wallet-5.13.3.tgz", - "integrity": "sha512-CwDipejJ64EGvCn0VohMXD8+4zX6lDE8KvYd7CHgmk+cT5HwQxO0WfGGL0m7HgWPpbDw5Eus/b6NXFQtRJIlxA==", - "license": "MIT", - "dependencies": { - "@chris.troutner/retry-queue-commonjs": "1.0.8", - "@psf/bch-js": "6.8.1", - "apidoc": "0.51.0", - "bch-consumer": "1.6.2", - "bch-donation": "1.1.2", - "crypto-js": "4.0.0" - } - }, - "node_modules/minimal-slp-wallet/node_modules/@psf/bch-js": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-6.8.1.tgz", - "integrity": "sha512-xu5YT9L3OhdILwJUmkV7Pg/WZ3r2aA6jD7fr5WMvo0OJSzzBBbXGShIbQNZp9+Vv9BoeVLCMqpZoW1Fp7OLCbg==", - "license": "MIT", - "dependencies": { - "@chris.troutner/bip32-utils": "1.0.5", - "@psf/bip21": "2.0.1", - "@psf/bitcoincash-ops": "2.0.0", - "@psf/bitcoincashjs-lib": "4.0.3", - "@psf/coininfo": "4.0.0", - "axios": "0.26.1", - "bc-bip68": "1.0.5", - "bchaddrjs-slp": "0.2.5", - "bigi": "1.4.2", - "bignumber.js": "9.0.0", - "bip-schnorr": "0.3.0", - "bip38": "2.0.2", - "bip39": "3.0.2", - "bip66": "1.1.5", - "bitcoinjs-message": "2.0.0", - "bs58": "4.0.1", - "ecashaddrjs": "1.0.7", - "ini": "1.3.8", - "randombytes": "2.0.6", - "safe-buffer": "5.1.2", - "satoshi-bitcoin": "1.0.4", - "slp-mdm": "0.0.6", - "slp-parser": "0.0.4", - "wif": "2.0.6" - } - }, - "node_modules/minimal-slp-wallet/node_modules/apidoc": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.51.0.tgz", - "integrity": "sha512-3P4srhm6NA+kE/YRM4qL5jESElpQQJL+Z8n7hyr4uC+1DSwGzmE6adXPVxuT/hpDyShrqmRlHk8hf40RSqEsNw==", - "license": "MIT", - "os": [ - "darwin", - "freebsd", - "linux", - "openbsd", - "win32" - ], - "dependencies": { - "bootstrap": "3.4.1", - "commander": "^8.3.0", - "diff-match-patch": "^1.0.5", - "esbuild-loader": "^2.16.0", - "expose-loader": "^3.1.0", - "fs-extra": "^10.0.0", - "glob": "^7.2.0", - "handlebars": "^4.7.7", - "iconv-lite": "^0.6.3", - "jquery": "^3.6.0", - "klaw-sync": "^6.0.0", - "lodash": "^4.17.21", - "markdown-it": "^12.2.0", - "nodemon": "^2.0.15", - "path-to-regexp": "^6.2.0", - "prismjs": "^1.25.0", - "semver": "^7.3.5", - "style-loader": "^3.3.1", - "url-parse": "^1.5.3", - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "winston": "^3.3.3" - }, - "bin": { - "apidoc": "bin/apidoc" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/minimal-slp-wallet/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/minimal-slp-wallet/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/minimal-slp-wallet/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/minimal-slp-wallet/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/minimal-slp-wallet/node_modules/expose-loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", - "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimal-slp-wallet/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/minimal-slp-wallet/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minimal-slp-wallet/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimal-slp-wallet/node_modules/nodemon": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", - "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^3.2.7", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/minimal-slp-wallet/node_modules/nodemon/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/minimal-slp-wallet/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "node_modules/minimal-slp-wallet/node_modules/randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/minimal-slp-wallet/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/minimal-slp-wallet/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/minimal-slp-wallet/node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "license": "MIT", - "dependencies": { - "semver": "~7.0.0" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/minimal-slp-wallet/node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/minimal-slp-wallet/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } + "resolved": "../minimal-slp-wallet", + "link": true }, "node_modules/minimalistic-assert": { "version": "1.0.1", @@ -7859,12 +7061,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8065,12 +7261,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -9599,16 +8789,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 7a42b15..fbb0450 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "cors": "2.8.5", "dotenv": "16.3.1", "express": "5.1.0", - "minimal-slp-wallet": "5.13.3", + "minimal-slp-wallet": "/home/trout/work/psf/code/api-dev/minimal-slp-wallet", "psffpp": "1.2.0", "slp-token-media": "1.2.10", "winston": "3.11.0", diff --git a/src/controllers/rest-api/encryption/controller.js b/src/controllers/rest-api/encryption/controller.js new file mode 100644 index 0000000..aa9c068 --- /dev/null +++ b/src/controllers/rest-api/encryption/controller.js @@ -0,0 +1,100 @@ +/* + REST API Controller for the /encryption routes. +*/ + +import wlogger from '../../../adapters/wlogger.js' + +class EncryptionRESTController { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating Encryption REST Controller.' + ) + } + + this.useCases = localConfig.useCases + if (!this.useCases || !this.useCases.encryption) { + throw new Error( + 'Instance of Encryption use cases required when instantiating Encryption REST Controller.' + ) + } + + this.encryptionUseCases = this.useCases.encryption + + // Bind functions + this.root = this.root.bind(this) + this.getPublicKey = this.getPublicKey.bind(this) + this.handleError = this.handleError.bind(this) + } + + /** + * @api {get} /v6/encryption/ Service status + * @apiName EncryptionRoot + * @apiGroup Encryption + * + * @apiDescription Returns the status of the encryption service. + * + * @apiSuccess {String} status Service identifier + */ + async root (req, res) { + return res.status(200).json({ status: 'encryption' }) + } + + /** + * @api {get} /v6/encryption/publickey/:address Get public key for a BCH address + * @apiName GetPublicKey + * @apiGroup Encryption + * @apiDescription Searches the blockchain for a public key associated with a + * BCH address. Returns an object. If successful, the publicKey property will + * contain a hexadecimal representation of the public key. + * + * @apiParam {String} address BCH address (cash address or legacy format) + * + * @apiExample Example usage: + * curl -X GET "https://api.fullstack.cash/v6/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json" + * + * @apiSuccess {Boolean} success Indicates if the operation was successful + * @apiSuccess {String} publicKey The public key in hexadecimal format, or "not found" + */ + async getPublicKey (req, res) { + try { + const address = req.params.address + + // Reject if address is an array + if (Array.isArray(address)) { + res.status(400) + return res.json({ + success: false, + error: 'address can not be an array.' + }) + } + + // Reject if address is missing + if (!address) { + res.status(400) + return res.json({ + success: false, + error: 'address is required.' + }) + } + + const result = await this.encryptionUseCases.getPublicKey({ address }) + + return res.status(200).json(result) + } catch (err) { + return this.handleError(err, res) + } + } + + handleError (err, res) { + wlogger.error('Error in EncryptionRESTController:', err) + + const status = err.status || 500 + const message = err.message || 'Internal server error' + + return res.status(status).json({ success: false, error: message }) + } +} + +export default EncryptionRESTController diff --git a/src/controllers/rest-api/encryption/router.js b/src/controllers/rest-api/encryption/router.js new file mode 100644 index 0000000..fb8e89e --- /dev/null +++ b/src/controllers/rest-api/encryption/router.js @@ -0,0 +1,51 @@ +/* + REST API router for /encryption routes. +*/ + +import express from 'express' +import EncryptionRESTController from './controller.js' + +class EncryptionRouter { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating Encryption REST Router.' + ) + } + + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error( + 'Instance of Use Cases library required when instantiating Encryption REST Router.' + ) + } + + const dependencies = { + adapters: this.adapters, + useCases: this.useCases + } + + this.encryptionController = new EncryptionRESTController(dependencies) + + this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '') + this.baseUrl = `${this.apiPrefix}/encryption` + if (!this.baseUrl.startsWith('/')) { + this.baseUrl = `/${this.baseUrl}` + } + this.router = express.Router() + } + + attach (app) { + if (!app) { + throw new Error('Must pass app object when attaching REST API controllers.') + } + + this.router.get('/', this.encryptionController.root) + this.router.get('/publickey/:address', this.encryptionController.getPublicKey) + + app.use(this.baseUrl, this.router) + } +} + +export default EncryptionRouter diff --git a/src/controllers/rest-api/index.js b/src/controllers/rest-api/index.js index 56917c1..7494c0b 100644 --- a/src/controllers/rest-api/index.js +++ b/src/controllers/rest-api/index.js @@ -10,6 +10,7 @@ import BlockchainRouter from './full-node/blockchain/router.js' import ControlRouter from './full-node/control/router.js' import DSProofRouter from './full-node/dsproof/router.js' +import EncryptionRouter from './encryption/router.js' import FulcrumRouter from './fulcrum/router.js' import MiningRouter from './full-node/mining/router.js' import PriceRouter from './price/router.js' @@ -70,6 +71,9 @@ class RESTControllers { const dsproofRouter = new DSProofRouter(dependencies) dsproofRouter.attach(app) + const encryptionRouter = new EncryptionRouter(dependencies) + encryptionRouter.attach(app) + const fulcrumRouter = new FulcrumRouter(dependencies) fulcrumRouter.attach(app) diff --git a/src/use-cases/encryption-use-cases.js b/src/use-cases/encryption-use-cases.js new file mode 100644 index 0000000..77ef1fd --- /dev/null +++ b/src/use-cases/encryption-use-cases.js @@ -0,0 +1,120 @@ +/* + Use cases for encryption-related operations. + Retrieves public keys from the blockchain for BCH addresses. +*/ + +// Global npm libraries +import BCHJS from '@psf/bch-js' + +// Local libraries +import wlogger from '../adapters/wlogger.js' +import config from '../config/index.js' + +const bchjs = new BCHJS({ restURL: config.restURL }) + +class EncryptionUseCases { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error('Adapters instance required when instantiating Encryption use cases.') + } + + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error('UseCases instance required when instantiating Encryption use cases.') + } + + // Allow bchjs to be injected for testing + this.bchjs = localConfig.bchjs || bchjs + } + + /** + * Get the public key for a BCH address by searching the blockchain. + * Searches the transaction history of the address for a transaction input + * that contains the public key. + * + * @param {Object} params - Parameters object + * @param {string} params.address - BCH address (cash address or legacy format) + * @returns {Promise} Object with success status and publicKey if found + */ + async getPublicKey ({ address }) { + try { + // Convert to cash address format + const cashAddr = this.bchjs.Address.toCashAddress(address) + + // Get transaction history for the address + const txHistory = await this.useCases.fulcrum.getTransactions({ address: cashAddr }) + + // Extract just the TXIDs + const txids = txHistory.transactions.map((elem) => elem.tx_hash) + + // Throw error if there is no transaction history + if (!txids || txids.length === 0) { + throw new Error('No transaction history.') + } + + // Loop through the transaction history and search for the public key + for (let i = 0; i < txids.length; i++) { + const thisTx = txids[i] + + // Get verbose transaction details + const txDetails = await this.useCases.rawtransactions.getRawTransaction({ + txid: thisTx, + verbose: true + }) + + const vin = txDetails.vin + + // Loop through each input + for (let j = 0; j < vin.length; j++) { + const thisVin = vin[j] + + // Skip if no scriptSig (e.g., coinbase transactions) + if (!thisVin.scriptSig || !thisVin.scriptSig.asm) { + continue + } + + // Extract the script signature + const scriptSig = thisVin.scriptSig.asm.split(' ') + + // Extract the public key from the script signature (last element) + const pubKey = scriptSig[scriptSig.length - 1] + + // Skip if pubKey is not a valid hex string (basic validation) + if (!pubKey || !/^[0-9a-fA-F]+$/.test(pubKey)) { + continue + } + + try { + // Generate cash address from public key + const keyBuf = Buffer.from(pubKey, 'hex') + const ec = this.bchjs.ECPair.fromPublicKey(keyBuf) + const cashAddr2 = this.bchjs.ECPair.toCashAddress(ec) + + // If public keys match, this is the correct public key + if (cashAddr === cashAddr2) { + return { + success: true, + publicKey: pubKey + } + } + } catch (err) { + // Skip invalid public keys - continue searching + continue + } + } + } + + // Public key not found in any transaction + return { + success: false, + publicKey: 'not found' + } + } catch (err) { + wlogger.error('Error in EncryptionUseCases.getPublicKey()', err) + throw err + } + } +} + +export default EncryptionUseCases diff --git a/src/use-cases/index.js b/src/use-cases/index.js index bbcb427..7021cb9 100644 --- a/src/use-cases/index.js +++ b/src/use-cases/index.js @@ -8,6 +8,7 @@ import BlockchainUseCases from './full-node-blockchain-use-cases.js' import ControlUseCases from './full-node-control-use-cases.js' import DSProofUseCases from './full-node-dsproof-use-cases.js' +import EncryptionUseCases from './encryption-use-cases.js' import FulcrumUseCases from './fulcrum-use-cases.js' import MiningUseCases from './full-node-mining-use-cases.js' import PriceUseCases from './price-use-cases.js' @@ -31,6 +32,12 @@ class UseCases { this.price = new PriceUseCases({ adapters: this.adapters }) this.rawtransactions = new RawTransactionsUseCases({ adapters: this.adapters }) this.slp = new SlpUseCases({ adapters: this.adapters }) + + // Encryption use cases require access to other use cases (fulcrum, rawtransactions) + this.encryption = new EncryptionUseCases({ + adapters: this.adapters, + useCases: this + }) } // Run any startup Use Cases at the start of the app. diff --git a/test/unit/controllers/encryption-controller-unit.js b/test/unit/controllers/encryption-controller-unit.js new file mode 100644 index 0000000..ccad9d7 --- /dev/null +++ b/test/unit/controllers/encryption-controller-unit.js @@ -0,0 +1,203 @@ +/* + Unit tests for EncryptionRESTController. +*/ + +import { assert } from 'chai' +import sinon from 'sinon' + +import EncryptionRESTController from '../../../src/controllers/rest-api/encryption/controller.js' +import { createMockRequest, createMockResponse, createMockRequestWithParams } from '../mocks/controller-mocks.js' + +describe('#encryption-controller.js', () => { + let sandbox + let mockAdapters + let mockUseCases + let uut + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockAdapters = {} + mockUseCases = { + encryption: { + getPublicKey: sandbox.stub().resolves({ + success: true, + publicKey: '02abc123def456789' + }) + } + } + + uut = new EncryptionRESTController({ + adapters: mockAdapters, + useCases: mockUseCases + }) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('#constructor()', () => { + it('should require adapters', () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new EncryptionRESTController({ useCases: mockUseCases }) + }, /Adapters library required/) + }) + + it('should require encryption use cases', () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new EncryptionRESTController({ adapters: mockAdapters, useCases: {} }) + }, /Encryption use cases required/) + }) + }) + + describe('#root()', () => { + it('should return encryption status', async () => { + const req = createMockRequest() + const res = createMockResponse() + + await uut.root(req, res) + + assert.equal(res.statusValue, 200) + assert.deepEqual(res.jsonData, { status: 'encryption' }) + }) + }) + + describe('#getPublicKey()', () => { + it('should return public key on success', async () => { + const req = createMockRequestWithParams({ + address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + }) + const res = createMockResponse() + + await uut.getPublicKey(req, res) + + assert.equal(res.statusValue, 200) + assert.deepEqual(res.jsonData, { + success: true, + publicKey: '02abc123def456789' + }) + assert.isTrue(mockUseCases.encryption.getPublicKey.calledOnce) + assert.isTrue(mockUseCases.encryption.getPublicKey.calledWith({ + address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + })) + }) + + it('should return not found when public key is not found', async () => { + mockUseCases.encryption.getPublicKey.resolves({ + success: false, + publicKey: 'not found' + }) + + const req = createMockRequestWithParams({ + address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + }) + const res = createMockResponse() + + await uut.getPublicKey(req, res) + + assert.equal(res.statusValue, 200) + assert.deepEqual(res.jsonData, { + success: false, + publicKey: 'not found' + }) + }) + + it('should reject array addresses', async () => { + const req = createMockRequestWithParams({ + address: ['addr1', 'addr2'] + }) + const res = createMockResponse() + + await uut.getPublicKey(req, res) + + assert.equal(res.statusValue, 400) + assert.deepEqual(res.jsonData, { + success: false, + error: 'address can not be an array.' + }) + assert.isFalse(mockUseCases.encryption.getPublicKey.called) + }) + + it('should reject missing address', async () => { + const req = createMockRequestWithParams({}) + const res = createMockResponse() + + await uut.getPublicKey(req, res) + + assert.equal(res.statusValue, 400) + assert.deepEqual(res.jsonData, { + success: false, + error: 'address is required.' + }) + assert.isFalse(mockUseCases.encryption.getPublicKey.called) + }) + + it('should handle errors via handleError', async () => { + const error = new Error('No transaction history.') + error.status = 400 + mockUseCases.encryption.getPublicKey.rejects(error) + + const req = createMockRequestWithParams({ + address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + }) + const res = createMockResponse() + + await uut.getPublicKey(req, res) + + assert.equal(res.statusValue, 400) + assert.deepEqual(res.jsonData, { + success: false, + error: 'No transaction history.' + }) + }) + + it('should default to 500 status for errors without status', async () => { + const error = new Error('Internal error') + mockUseCases.encryption.getPublicKey.rejects(error) + + const req = createMockRequestWithParams({ + address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' + }) + const res = createMockResponse() + + await uut.getPublicKey(req, res) + + assert.equal(res.statusValue, 500) + assert.deepEqual(res.jsonData, { + success: false, + error: 'Internal error' + }) + }) + }) + + describe('#handleError()', () => { + it('should use error status and message when provided', () => { + const error = new Error('Custom error') + error.status = 422 + const res = createMockResponse() + + uut.handleError(error, res) + + assert.equal(res.statusValue, 422) + assert.deepEqual(res.jsonData, { + success: false, + error: 'Custom error' + }) + }) + + it('should default to 500 and Internal server error', () => { + const error = {} + const res = createMockResponse() + + uut.handleError(error, res) + + assert.equal(res.statusValue, 500) + assert.deepEqual(res.jsonData, { + success: false, + error: 'Internal server error' + }) + }) + }) +}) diff --git a/test/unit/controllers/rest-api-index-unit.js b/test/unit/controllers/rest-api-index-unit.js index 61f60c1..3051a16 100644 --- a/test/unit/controllers/rest-api-index-unit.js +++ b/test/unit/controllers/rest-api-index-unit.js @@ -9,6 +9,7 @@ import RESTControllers from '../../../src/controllers/rest-api/index.js' import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/router.js' import ControlRouter from '../../../src/controllers/rest-api/full-node/control/router.js' import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/router.js' +import EncryptionRouter from '../../../src/controllers/rest-api/encryption/router.js' import MiningRouter from '../../../src/controllers/rest-api/full-node/mining/router.js' import PriceRouter from '../../../src/controllers/rest-api/price/router.js' import RawTransactionsRouter from '../../../src/controllers/rest-api/full-node/rawtransactions/router.js' @@ -100,6 +101,9 @@ describe('#controllers/rest-api/index.js', () => { getMutableCid: () => {}, decodeOpReturn: () => {}, getCIDData: () => {} + }, + encryption: { + getPublicKey: () => {} } } }) @@ -129,6 +133,7 @@ describe('#controllers/rest-api/index.js', () => { const blockchainAttachStub = sandbox.stub(BlockchainRouter.prototype, 'attach') const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach') const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach') + const encryptionAttachStub = sandbox.stub(EncryptionRouter.prototype, 'attach') const fulcrumAttachStub = sandbox.stub(FulcrumRouter.prototype, 'attach') const miningAttachStub = sandbox.stub(MiningRouter.prototype, 'attach') const priceAttachStub = sandbox.stub(PriceRouter.prototype, 'attach') @@ -148,6 +153,8 @@ describe('#controllers/rest-api/index.js', () => { assert.equal(controlAttachStub.getCall(0).args[0], app) assert.isTrue(dsproofAttachStub.calledOnce) assert.equal(dsproofAttachStub.getCall(0).args[0], app) + assert.isTrue(encryptionAttachStub.calledOnce) + assert.equal(encryptionAttachStub.getCall(0).args[0], app) assert.isTrue(fulcrumAttachStub.calledOnce) assert.equal(fulcrumAttachStub.getCall(0).args[0], app) assert.isTrue(miningAttachStub.calledOnce) diff --git a/test/unit/use-cases/encryption-use-cases-unit.js b/test/unit/use-cases/encryption-use-cases-unit.js new file mode 100644 index 0000000..565e307 --- /dev/null +++ b/test/unit/use-cases/encryption-use-cases-unit.js @@ -0,0 +1,247 @@ +/* + Unit tests for EncryptionUseCases. +*/ + +import { assert } from 'chai' +import sinon from 'sinon' + +import EncryptionUseCases from '../../../src/use-cases/encryption-use-cases.js' + +describe('#encryption-use-cases.js', () => { + let sandbox + let mockAdapters + let mockUseCases + let mockBchjs + let uut + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockAdapters = {} + + // Mock bchjs + mockBchjs = { + Address: { + toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') + }, + ECPair: { + fromPublicKey: sandbox.stub().returns({}), + toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') + } + } + + // Mock use cases + mockUseCases = { + fulcrum: { + getTransactions: sandbox.stub().resolves({ + transactions: [ + { tx_hash: 'abc123def456' } + ] + }) + }, + rawtransactions: { + getRawTransaction: sandbox.stub().resolves({ + vin: [ + { + scriptSig: { + asm: 'signature 02abc123def456789' + } + } + ] + }) + } + } + + uut = new EncryptionUseCases({ + adapters: mockAdapters, + useCases: mockUseCases, + bchjs: mockBchjs + }) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('#constructor()', () => { + it('should require adapters', () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new EncryptionUseCases({ useCases: mockUseCases }) + }, /Adapters instance required/) + }) + + it('should require useCases', () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new EncryptionUseCases({ adapters: mockAdapters }) + }, /UseCases instance required/) + }) + }) + + describe('#getPublicKey()', () => { + it('should return public key when found', async () => { + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isTrue(result.success) + assert.equal(result.publicKey, '02abc123def456789') + assert.isTrue(mockBchjs.Address.toCashAddress.calledOnce) + assert.isTrue(mockUseCases.fulcrum.getTransactions.calledOnce) + assert.isTrue(mockUseCases.rawtransactions.getRawTransaction.calledOnce) + }) + + it('should return not found when public key does not match', async () => { + // Make the ECPair.toCashAddress return a different address + mockBchjs.ECPair.toCashAddress.returns('bitcoincash:qqq000000000000000000000000000000000000000') + + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isFalse(result.success) + assert.equal(result.publicKey, 'not found') + }) + + it('should throw error when no transaction history', async () => { + mockUseCases.fulcrum.getTransactions.resolves({ + transactions: [] + }) + + try { + await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + assert.fail('Should have thrown an error') + } catch (err) { + assert.equal(err.message, 'No transaction history.') + } + }) + + it('should handle transactions without scriptSig', async () => { + mockUseCases.rawtransactions.getRawTransaction.resolves({ + vin: [ + { txid: 'coinbase' } // No scriptSig + ] + }) + + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isFalse(result.success) + assert.equal(result.publicKey, 'not found') + }) + + it('should handle invalid public key hex gracefully', async () => { + mockUseCases.rawtransactions.getRawTransaction.resolves({ + vin: [ + { + scriptSig: { + asm: 'signature NOT_VALID_HEX' + } + } + ] + }) + + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isFalse(result.success) + assert.equal(result.publicKey, 'not found') + }) + + it('should handle ECPair.fromPublicKey throwing error', async () => { + mockBchjs.ECPair.fromPublicKey.throws(new Error('Invalid public key')) + + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isFalse(result.success) + assert.equal(result.publicKey, 'not found') + }) + + it('should search through multiple transactions', async () => { + // First transaction has no matching public key + mockUseCases.fulcrum.getTransactions.resolves({ + transactions: [ + { tx_hash: 'tx1' }, + { tx_hash: 'tx2' } + ] + }) + + // Return different data for each tx - first tx has input with non-matching key + mockUseCases.rawtransactions.getRawTransaction + .onFirstCall().resolves({ + vin: [ + { + scriptSig: { + asm: 'sig 02aaa111bbb222ccc' + } + } + ] + }) + .onSecondCall().resolves({ + vin: [ + { + scriptSig: { + asm: 'sig 02abc123def456789' + } + } + ] + }) + + // First tx doesn't match, second tx matches + mockBchjs.ECPair.toCashAddress + .onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000') + .onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') + + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isTrue(result.success) + assert.equal(result.publicKey, '02abc123def456789') + assert.equal(mockUseCases.rawtransactions.getRawTransaction.callCount, 2) + }) + + it('should search through multiple inputs in a transaction', async () => { + mockUseCases.rawtransactions.getRawTransaction.resolves({ + vin: [ + { + scriptSig: { + asm: 'sig 02aaa111bbb222ccc' + } + }, + { + scriptSig: { + asm: 'sig 02abc123def456789' + } + } + ] + }) + + // First input doesn't match, second input matches + mockBchjs.ECPair.toCashAddress + .onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000') + .onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') + + const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + + assert.isTrue(result.success) + assert.equal(result.publicKey, '02abc123def456789') + }) + + it('should propagate fulcrum errors', async () => { + const error = new Error('Fulcrum API error') + mockUseCases.fulcrum.getTransactions.rejects(error) + + try { + await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + assert.fail('Should have thrown an error') + } catch (err) { + assert.equal(err.message, 'Fulcrum API error') + } + }) + + it('should propagate rawtransactions errors', async () => { + const error = new Error('RawTransactions API error') + mockUseCases.rawtransactions.getRawTransaction.rejects(error) + + try { + await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' }) + assert.fail('Should have thrown an error') + } catch (err) { + assert.equal(err.message, 'RawTransactions API error') + } + }) + }) +})