diff --git a/packages/bitcore-cli/copyTestWallets b/packages/bitcore-cli/copyTestWallets new file mode 100644 index 00000000000..64038431b5a --- /dev/null +++ b/packages/bitcore-cli/copyTestWallets @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const sourceDir = path.join(__dirname, 'test/wallets'); +const targetDir = path.join(__dirname, 'build/test/wallets'); + +if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); +} + +const srcContents = fs.readdirSync(sourceDir); +for (const item of srcContents) { + const srcPath = path.join(sourceDir, item); + const targetPath = path.join(targetDir, item); + fs.copyFileSync(srcPath, targetPath); +} diff --git a/packages/bitcore-cli/package-lock.json b/packages/bitcore-cli/package-lock.json index 688849df9d4..c6a4006f2fc 100644 --- a/packages/bitcore-cli/package-lock.json +++ b/packages/bitcore-cli/package-lock.json @@ -22,9 +22,11 @@ "@types/node": "^22.14.1", "chai": "^5.2.1", "mocha": "^11.7.1", + "mongodb": "^3.5.9", "nyc": "^17.1.0", "sinon": "^21.0.0", "source-map-support": "0.5.16", + "supertest": "^7.2.2", "typescript": "^5.8.3" } }, @@ -491,6 +493,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -653,6 +678,13 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -663,6 +695,13 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -683,6 +722,17 @@ "node": ">=6.0.0" } }, + "node_modules/bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -734,6 +784,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -757,6 +817,37 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -975,6 +1066,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", @@ -991,6 +1095,16 @@ "dev": true, "license": "MIT" }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1005,6 +1119,30 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1074,6 +1212,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -1084,6 +1253,21 @@ "node": ">=0.3.1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1105,6 +1289,55 @@ "dev": true, "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -1163,6 +1396,13 @@ "node": ">=4" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -1225,6 +1465,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -1253,6 +1528,16 @@ "dev": true, "license": "ISC" }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1273,6 +1558,31 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -1283,6 +1593,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -1305,6 +1629,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1322,6 +1659,35 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -1339,6 +1705,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1480,6 +1859,13 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1763,6 +2149,70 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -1826,6 +2276,46 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/mongodb": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz", + "integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "optional-require": "^1.1.8", + "safe-buffer": "^5.1.2" + }, + "engines": { + "node": ">=4" + }, + "optionalDependencies": { + "saslprep": "^1.0.0" + }, + "peerDependenciesMeta": { + "aws4": { + "optional": true + }, + "bson-ext": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "mongodb-extjson": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2140,6 +2630,19 @@ "node": ">=6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2150,6 +2653,19 @@ "wrappy": "1" } }, + "node_modules/optional-require": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.10.tgz", + "integrity": "sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "require-at": "^1.0.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -2376,6 +2892,13 @@ "node": ">=8" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/process-on-spawn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", @@ -2389,6 +2912,22 @@ "node": ">=8" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2399,6 +2938,29 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/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==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2426,6 +2988,16 @@ "node": ">=4" } }, + "node_modules/require-at": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", + "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2543,6 +3115,20 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -2596,6 +3182,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2677,6 +3339,17 @@ "source-map": "^0.6.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -2723,6 +3396,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/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==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2850,6 +3540,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3036,6 +3762,13 @@ "node": ">=12.22.0 <13.0 || >=14.17.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/packages/bitcore-cli/package.json b/packages/bitcore-cli/package.json index e291ea3baf9..198db42d267 100644 --- a/packages/bitcore-cli/package.json +++ b/packages/bitcore-cli/package.json @@ -30,10 +30,12 @@ }, "scripts": { - "test": "npm run compile && mocha 'build/test/**/*.js'", - "coverage": "npm run compile && nyc mocha 'build/test/**/*.js'", + "test": "npm run compile && node copyTestWallets && mocha --exit 'build/test/**/*.js'", + "coverage": "npm run compile && node copyTestWallets && nyc mocha --exit 'build/test/**/*.js'", "build": "tsc", + "build:prod": "tsc -p tsconfig.prod.json", "postbuild": "node createBin -v", + "postbuild:prod": "npm run postbuild", "clean": "rm -rf ./build", "compile": "npm run clean && npm run build", "lint": "eslint .", @@ -52,14 +54,17 @@ "usb": "2.15.0" }, "devDependencies": { + "@bitpay-labs/bitcore-wallet-service": "^11.6.6", "@types/chai": "5.2.2", "@types/mocha": "^10.0.10", "@types/node": "^22.14.1", "chai": "^5.2.1", "mocha": "^11.7.1", + "mongodb": "^3.5.9", "nyc": "^17.1.0", "sinon": "^21.0.0", "source-map-support": "0.5.16", + "supertest": "^7.2.2", "typescript": "^5.8.3" }, "nyc": { diff --git a/packages/bitcore-cli/src/commands/token.ts b/packages/bitcore-cli/src/commands/token.ts index 96ef6230429..6f68c219868 100644 --- a/packages/bitcore-cli/src/commands/token.ts +++ b/packages/bitcore-cli/src/commands/token.ts @@ -11,9 +11,11 @@ export async function setToken(args: CommonArgs) { const currencies = await Wallet.getCurrencies(wallet.network); function findTokenObj(value) { return currencies.find(c => - c.contractAddress?.toLowerCase() === value.toLowerCase() || - c.displayCode?.toLowerCase() === value.toLowerCase() || - c.code?.toLowerCase() === value.toLowerCase() + c.chain?.toUpperCase() === wallet.chain.toUpperCase() && ( + c.contractAddress?.toLowerCase() === value.toLowerCase() || + c.displayCode?.toLowerCase() === value.toLowerCase() || + c.code?.toLowerCase() === value.toLowerCase() + ) ); }; diff --git a/packages/bitcore-cli/src/commands/txproposals.ts b/packages/bitcore-cli/src/commands/txproposals.ts index 612c2a57ffd..984f030e643 100755 --- a/packages/bitcore-cli/src/commands/txproposals.ts +++ b/packages/bitcore-cli/src/commands/txproposals.ts @@ -3,18 +3,19 @@ import os from 'os'; import * as prompt from '@clack/prompts'; import { ITokenObj } from '../../types/wallet'; import { UserCancelled } from '../errors'; -import { getAction, getFileName } from '../prompts'; +import { getFileName } from '../prompts'; import { Utils } from '../utils'; import type { CommonArgs } from '../../types/cli'; export function command(args: CommonArgs) { const { wallet, program } = args; program - .description('View, sign, and reject transaction proposals for a wallet') + .description('View or perform actions on transaction proposals for a wallet') .usage(' --command txproposals [options]') .optionsGroup('Tx Proposals Options') .option('--action ', 'Action to perform on transaction proposals: sign, reject, delete, broadcast') .option('--proposalId ', 'ID of the transaction proposal to act upon') + .option('--page ', 'Page number to view (only 1 proposal is displayed per page)') .option('--raw', 'Print raw transaction proposal objects instead of formatted output') .option('--export [filename]', `Export the transaction proposal(s) to a file(s) (default: ~/${wallet.name}_txproposal_.json)`) .parse(process.argv); @@ -27,6 +28,9 @@ export function command(args: CommonArgs) { if (!!opts.action !== !!opts.proposalId) { throw new Error('Both --action and --proposalId options must be provided together.'); } + if (!!opts.proposalId === !!opts.page) { + throw new Error('--page option does not make sense with --proposalId.'); + } return opts; } @@ -37,6 +41,7 @@ export async function getTxProposals( proposalId?: string; raw?: boolean; export?: string | boolean; + page?: number | string; }> ) { const { wallet, opts } = args; @@ -52,16 +57,93 @@ export async function getTxProposals( forAirGapped: false, // TODO }); - let action: string | symbol | undefined; - let i = 0; + enum ViewAction { + ACCEPT = 'a', + REJECT = 'j', + BROADCAST = 'b', + DELETE = 'd', + TOGGLE_RAW = 'r', + EXPORT = 'e' + } + + let lastPage = 1; let printRaw = opts.raw ?? false; + let txp; + + await Utils.paginate(async (page, action) => { + const i = page - 1; + txp = txps[i]; - do { - const txp = txps[i]; if (!txp) { prompt.log.info('No more proposals'); + return { result: [] }; + } + + const _txps = page < txps.length ? [txp] : [txp, { /* This element will prevent the paginator from showing Next Page option */}]; + + if (action === ViewAction.TOGGLE_RAW) { + printRaw = !printRaw; + } else if (lastPage !== page) { + printRaw = false; // reset to formatted view when changing pages + } + lastPage = page; + + + if (action === ViewAction.ACCEPT) { + txps[i] = await wallet.signAndBroadcastTxp({ txp }); + txp = txps[i]; + if (txp.status === 'broadcasted') { + prompt.log.success(`Broadcasted txid: ${Utils.colorText(txp.txid, 'green')}`); + } else { + prompt.log.info(`Proposal ${txp.id} signed. More signatures needed to broadcast.`); + } + + } else if (action === ViewAction.REJECT) { + const rejectReason = await prompt.text({ message: 'Enter rejection reason:' }); + if (prompt.isCancel(rejectReason)) { + throw new UserCancelled(); + } + txps[i] = await wallet.client.rejectTxProposal(txp, rejectReason); + txp = txps[i]; + + } else if (action === ViewAction.BROADCAST) { + txps[i] = await wallet.client.broadcastTxProposal(txp); + if (txps[i].status === 'broadcasted') { + prompt.log.success(`Broadcasted txid: ${Utils.colorText(txp.txid, 'green')}`); + } + txp = txps[i]; + + } else if (action === ViewAction.DELETE) { + const confirmDelete = await prompt.confirm({ + message: `Are you sure you want to delete proposal ${txp.id}?`, + initialValue: false + }); + if (prompt.isCancel(confirmDelete)) { + throw new UserCancelled(); + } + if (confirmDelete) { + await wallet.client.removeTxProposal(txp); + txps[i].status = 'deleted'; // Update status locally since it's removed from server + + prompt.log.success(`Proposal ${txp.id} deleted.`); + } else { + prompt.log.step(`Proposal ${txp.id} not deleted.`); + } + + } else if (action === ViewAction.EXPORT) { + const defaultValue = `~/${wallet.name}_txproposal_${txp.id}.json`; + const outputFile = opts.command + ? Utils.replaceTilde(typeof opts.export === 'string' ? opts.export : defaultValue) + : await getFileName({ + message: 'Enter output file path to save proposal:', + defaultValue, + }); + fs.writeFileSync(outputFile, JSON.stringify(txp, null, 2)); + prompt.log.success(`Exported to ${outputFile}`); + } else if (printRaw) { prompt.log.info(`ID: ${txp.id}` + os.EOL + JSON.stringify(txp, null, 2)); + } else { const lines = []; const chain = txp.chain || txp.coin; @@ -119,126 +201,35 @@ export async function getTxProposals( prompt.note(lines.join(os.EOL), `ID: ${txp.id}`); } - const options = []; - let initialValue; - - if (txp) { - if (txp.status !== 'broadcasted' && !txp.actions.find(a => a.copayerId === myCopayerId)) { - options.push({ label: 'Accept', value: 'accept', hint: 'Accept and sign this proposal' }); - options.push({ label: 'Reject', value: 'reject', hint: 'Reject this proposal' }); - initialValue = 'accept'; - } - if (txp.status !== 'broadcasted' && txp.actions.filter(a => a.type === 'accept').length >= txp.requiredSignatures) { - options.push({ label: 'Broadcast', value: 'broadcast', hint: 'Broadcast this proposal' }); - initialValue = 'broadcast'; - } - if (i > 0) { - options.push({ label: 'Previous', value: 'prev' }); - initialValue = 'prev'; - } - if (i < txps.length - 1) { - options.push({ label: 'Next', value: 'next' }); - initialValue = 'next'; - } - if (printRaw) { - options.push({ label: 'Print Pretty', value: 'pretty' }); - } else { - options.push({ label: 'Print Raw Object', value: 'raw' }); - } - if (txp.status !== 'broadcasted') { - options.push({ label: 'Delete', value: 'delete', hint: 'Delete this proposal' }); - } - options.push({ label: 'Export', value: 'export', hint: 'Save to a file' }); - } - - action = opts.command - ? opts.action || (opts.export ? 'export' : 'exit') - : await getAction({ - options, - initialValue - }); - if (prompt.isCancel(action)) { - throw new UserCancelled(); - } - - switch (action) { - case 'accept': - txps[i] = await wallet.signAndBroadcastTxp({ txp }); - if (txps[i].status === 'broadcasted') { - prompt.log.success(`Proposal ${txp.id} broadcasted.`); - } else { - prompt.log.info(`Proposal ${txps[i].id} signed. More signatures needed to broadcast.`); - } - break; - case 'reject': - const rejectReason = await prompt.text({ - message: 'Enter rejection reason:' - }); - if (prompt.isCancel(rejectReason)) { - throw new UserCancelled(); - } - txps[i] = await wallet.client.rejectTxProposal(txp, rejectReason); - break; - case 'broadcast': - txps[i] = await wallet.client.broadcastTxProposal(txp); - if (txps[i].status === 'broadcasted') { - prompt.log.success(`Proposal ${txp.id} broadcasted.`); - } - break; - case 'prev': - i--; - printRaw = false; - break; - case 'next': - i++; - printRaw = false; - break; - case 'raw': - case 'pretty': - printRaw = !printRaw; - break; - case 'delete': - const confirmDelete = await prompt.confirm({ - message: `Are you sure you want to delete proposal ${txp.id}?`, - initialValue: false - }); - if (prompt.isCancel(confirmDelete)) { - throw new UserCancelled(); - } - if (confirmDelete) { - await wallet.client.removeTxProposal(txp); - txps.splice(i, 1); - if (i >= txps.length) { - i = txps.length - 1; // adjust index if we deleted the last item - } - prompt.log.success(`Proposal ${txp.id} deleted.`); - } else { - prompt.log.step(`Proposal ${txp.id} not deleted.`); - } - break; - case 'export': - const defaultValue = `~/${wallet.name}_txproposal_${txp.id}.json`; - const outputFile = opts.command - ? Utils.replaceTilde(typeof opts.export === 'string' ? opts.export : defaultValue) - : await getFileName({ - message: 'Enter output file path to save proposal:', - defaultValue, - }); - fs.writeFileSync(outputFile, JSON.stringify(txp, null, 2)); - prompt.log.success(`Exported to ${outputFile}`); - break; - case 'menu': - case 'exit': - break; - default: - if (opts.command) throw new Error(`Unknown action: ${action}`); - } - if (opts.command) { - action = 'exit'; // Exit after processing the action in command mode + return {}; // Don't wait for user input in CLI mode } - // TODO: handle actions - } while (!['menu', 'exit'].includes(action)); - return { action }; + const extraChoices = [] + .concat( + txp.status !== 'broadcasted' && !txp.actions.find(a => a.copayerId === myCopayerId) && txp.status !== 'deleted' ? [ + { label: 'Accept', value: ViewAction.ACCEPT, hint: 'Accept and sign this proposal' }, + { label: 'Reject', value: ViewAction.REJECT, hint: 'Reject this proposal' }, + ] : [] + ).concat( + txp.status !== 'broadcasted' && txp.actions.filter(a => a.type === 'accept').length >= txp.requiredSignatures && txp.status !== 'deleted' ? [ + { label: 'Broadcast', value: ViewAction.BROADCAST, hint: 'Broadcast this proposal' } + ] : [] + ).concat( + txp.status !== 'broadcasted' && txp.status !== 'rejected' && txp.status !== 'deleted' ? [ + { label: 'Delete', value: ViewAction.DELETE, hint: 'Delete this proposal' } + ] : [] + ).concat([ + printRaw ? { label: 'Print Pretty', value: ViewAction.TOGGLE_RAW, hint: 'Print formatted proposal' } : { label: 'Print Raw Object', value: ViewAction.TOGGLE_RAW, hint: 'Print raw proposal object' }, + { label: 'Export', value: ViewAction.EXPORT, hint: 'Save to a file' }, + ]); + + return { result: _txps, extraChoices }; + }, { + pageSize: 1, + initialPage: opts.page, + exitOn1Page: !!opts.command + }); + + return { action: 'menu' }; }; \ No newline at end of file diff --git a/packages/bitcore-cli/src/utils.ts b/packages/bitcore-cli/src/utils.ts index 04a3eeec9b1..5dd9e8f36c9 100644 --- a/packages/bitcore-cli/src/utils.ts +++ b/packages/bitcore-cli/src/utils.ts @@ -30,14 +30,25 @@ export class Utils { static goodbye() { const funMessages = [ 'Until next time!', - 'See you later!', 'Keep calm and HODL on!', 'Goodbye!', - 'Tata!', - 'Chin-chin!', 'Cheers!', - 'Adios!', - 'Ciao!', + 'Goodbye, and may your transactions always confirm quickly!', + 'Thanks for using Bitcore CLI!', + 'Adiós!', // Spanish + 'Ciao!', // Italian (informal) + 'Arrivederci!', // Italian (formal) + 'Tchau!', // Portuguese + 'Salut!', // French (informal) + 'Au revoir!', // French (formal) + 'Tschüss!', // German (informal) + 'Auf Wiedersehen!', // German (formal) + 'さようなら (Sayōnara)!', // Japanese + 'до свидания (Do svidaniya)!', // Russian (formal) + 'пока (Poka)!', // Russian (informal) + 'Aloha!', // Hawaiian + '안녕히 가세요 (Annyeonghi gaseyo)!', // Korean + '再见 (Zàijiàn)!', // Chinese/Mandarin ]; const randomMessage = funMessages[Math.floor(Math.random() * funMessages.length)]; console.log('👋 ' + randomMessage); @@ -137,11 +148,23 @@ export class Utils { } static async paginate( - fn: (page: number, action?: string) => Promise<{ result?: any[]; extraChoices?: prompt.Option[] }>, + /** Body function to handle calling for and display of data */ + fn: ( + /** Page number to display */ + page: number, + /** Action to perform on the data */ + viewAction?: string + ) => Promise<{ + /** Data used to display on the current page */ + result?: any[]; + /** Extra choices to show in the pagination menu */ + extraChoices?: prompt.Option[]; + }>, opts?: { pageSize?: number; - initialPage?: number | string; // Initial page, default is 1 - /** Only applies if there are no extraChoices */ + /** Default: 1 */ + initialPage?: number | string; + /** Do not show pagination controls if there is only one page. Only applies if there are no extraChoices */ exitOn1Page?: boolean; } ) { diff --git a/packages/bitcore-cli/src/wallet.ts b/packages/bitcore-cli/src/wallet.ts index 770dc67f0cf..f701864869c 100644 --- a/packages/bitcore-cli/src/wallet.ts +++ b/packages/bitcore-cli/src/wallet.ts @@ -257,13 +257,10 @@ export class Wallet implements IWallet { if (doNotComplete) return key; - - this.client.on('walletCompleted', (_wallet) => { - this.save().then(() => { - _verbose && prompt.log.info('Your wallet has just been completed.'); - }); - }); - await this.client.openWallet(); + const status = await this.client.openWallet(); + if (status?.wallet?.status === 'complete') { + await this.save(); + } return key; }; @@ -362,6 +359,9 @@ export class Wallet implements IWallet { testnet: process.env['BITCORE_CLI_CURRENCIES_URL'] || 'https://test.bitpay.com/currencies', regtest: process.env['BITCORE_CLI_CURRENCIES_URL_REGTEST'] }; + if (network === 'regtest' && !urls[network]) { + throw new Error('Set BITCORE_CLI_CURRENCIES_URL_REGTEST environment variable.'); + } let response: Response; try { response = await fetch(urls[network], { method: 'GET', headers: { 'Content-Type': 'application/json' } }); diff --git a/packages/bitcore-cli/test/address.test.ts b/packages/bitcore-cli/test/address.test.ts new file mode 100644 index 00000000000..56dad5c0e43 --- /dev/null +++ b/packages/bitcore-cli/test/address.test.ts @@ -0,0 +1,271 @@ +import { spawn } from 'child_process'; +import assert from 'assert'; +import { Transform } from 'stream'; +import * as helpers from './helpers'; +import * as walletData from './data/walletsData'; +import * as addressesData from './data/addressesData'; + +describe('Address', function() { + this.timeout(Math.max(this['_timeout'] || 0, 5000)); + const { KEYSTROKES, WALLETS } = helpers.CONSTANTS; + const { CLI_EXEC, COMMON_OPTS, DIR } = WALLETS; + const cmdOpts = [...COMMON_OPTS, '--dir', DIR]; + + before(async function() { + await helpers.startBws(); + await helpers.loadWalletData(walletData.btcSingleSigWallet); + }); + + after(async function() { + await helpers.stopBws(); + }); + + it('should show no addresses for a new wallet', function(done) { + const stepInputs = [ + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ARROW_UP], // Exit -> Show Advanced + [KEYSTROKES.ENTER], // Show Advanced Options ...-> Message + [KEYSTROKES.ARROW_DOWN], // Message -> Addresses + [KEYSTROKES.ENTER], // Addresses + // Checkpoint1: Addresses view shows no addresses for a new wallet + ['x'], // Page Controls: Close -- (checkpoint1) + [KEYSTROKES.ARROW_DOWN], // Main Menu -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([5]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + if (step === Array.from(checkpoints)[0]) { + // Assert addresses output contains expected info for no addresses + assert.match(checkpointOutput, /Addresses \(Page 1\)/); + assert.doesNotMatch(checkpointOutput, /bcrt1/); + } + step++; + } else if (chunk.includes('Error:')) { + return respond(chunk); + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + + it('should generate new addresses', function(done) { + const stepInputs = [ + [KEYSTROKES.ARROW_DOWN], // Proposals -> Send + [KEYSTROKES.ARROW_DOWN], // Send -> Recieve + [KEYSTROKES.ENTER], // Recieve + // Checkpoint1: Address view shows first generated address (m/0/0) + [KEYSTROKES.ENTER], // Main Menu -- (checkpoint1) + [KEYSTROKES.ARROW_DOWN], // Proposals -> Send + [KEYSTROKES.ARROW_DOWN], // Send -> Recieve + [KEYSTROKES.ENTER], // Recieve + // Checkpoint2: Address view shows second generated address (m/0/1) + [KEYSTROKES.ENTER], // Main Menu -- (checkpoint2) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ARROW_UP], // Exit -> Show Advanced + [KEYSTROKES.ENTER], // Show Advanced Options ...-> Message + [KEYSTROKES.ARROW_DOWN], // Message -> Addresses + [KEYSTROKES.ENTER], // Addresses + // Checkpoint3: Addresses view shows both generated addresses (m/0/0 and m/0/1) + ['x'], // Page Controls: Close -- (checkpoint3) + [KEYSTROKES.ARROW_DOWN], // Main Menu -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([3, 7, 13]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert address output contains expected info for first generated address + assert.match(checkpointOutput, /Address \(m\/0\/0\)/); + assert.match(checkpointOutput, /bcrt1q/); + break; + case Array.from(checkpoints)[1]: + // Assert address output contains expected info for second generated address + assert.match(checkpointOutput, /Address \(m\/0\/1\)/); + assert.match(checkpointOutput, /bcrt1q/); + break; + case Array.from(checkpoints)[2]: + // Assert addresses output contains expected info for both generated addresses + assert.match(checkpointOutput, /Addresses \(Page 1\)/); + assert.match(checkpointOutput, /bcrt1q[a-z0-9]+ \(m\/0\/0\)/); + assert.match(checkpointOutput, /bcrt1q[a-z0-9]+ \(m\/0\/1\)/); + break; + } + step++; + } else if (chunk.includes('Error:')) { + return respond(chunk); + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + describe('Pagination', function() { + beforeEach(async function() { + await helpers.loadWalletAddressData(walletData.btcSingleSigWallet, addressesData.addressesBtcSingleSig.filter(a => parseInt(a.path.split('/')[2]) > 1)); + }); + it('should paginate addresses', function(done) { + const stepInputs = [ + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ARROW_UP], // Exit -> Show Advanced + [KEYSTROKES.ENTER], // Show Advanced Options ...-> Message + [KEYSTROKES.ARROW_DOWN], // Message -> Addresses + [KEYSTROKES.ENTER], // Addresses + // Checkpoint1: Addresses view shows addresses (page 1) + ['n'], // Page 1 -> Page 2 -- (checkpoint1) + // Checkpoint2: Addresses view shows addresses (page 2) + ['x'], // Page Controls: Close -- (checkpoint2) + [KEYSTROKES.ARROW_DOWN], // Main Menu -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([5, 6]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert addresses output contains expected info for both generated addresses + assert.match(checkpointOutput, /Addresses \(Page 1\)/); + assert.match(checkpointOutput, /bcrt1q6l953jevexkqrvvah8729nud289djcpae9jkx4 \(m\/0\/0\)/); + assert.match(checkpointOutput, /bcrt1qdq929kz9r7adapvruevgz0nkkqd3cpfvgh7wny \(m\/0\/1\)/); + assert.match(checkpointOutput, /bcrt1qqr57cev8t25sph9qksdvslf80v9vy2nrlpwarz \(m\/0\/2\)/); + assert.match(checkpointOutput, /bcrt1quug3ztz5hgqe053hs2jzds70n0uynppu2lfyp3 \(m\/0\/3\)/); + assert.match(checkpointOutput, /bcrt1q0xp8938csu3rg9zxru7xfxer25ynzjzt3prh68 \(m\/0\/4\)/); + assert.match(checkpointOutput, /bcrt1qpn6lwuj30vdhjrl86pkxashmgf923c0jrv7vxc \(m\/0\/5\)/); + assert.match(checkpointOutput, /bcrt1qqz5lc5wttuk2u5ntf0ptjjrpexs8n4upxg0hwe \(m\/0\/6\)/); + assert.match(checkpointOutput, /bcrt1qdgv30yrsmlu790j40nm3mk895296va4xjyqar2 \(m\/0\/7\)/); + assert.match(checkpointOutput, /bcrt1q3s69dnlf2jnm50eaxxp2xyy8h5t7tah8yp354s \(m\/0\/8\)/); + assert.match(checkpointOutput, /bcrt1qk93dstvzpyk5vpj9zt4gxzvsayuqvhkv6qws0e \(m\/0\/9\)/); + assert.doesNotMatch(checkpointOutput, /bcrt1qng4qgjrdqxx8n87pk5mnzvm2u6k3xjvgnt06zx \(m\/0\/10\)/); + break; + case Array.from(checkpoints)[1]: + // Assert addresses output contains expected info for both generated addresses + assert.match(checkpointOutput, /Addresses \(Page 2\)/); + assert.match(checkpointOutput, /bcrt1qng4qgjrdqxx8n87pk5mnzvm2u6k3xjvgnt06zx \(m\/0\/10\)/); + assert.match(checkpointOutput, /bcrt1q7kle0glqvheed9rykchzfs7nksfznnqygtn0my \(m\/0\/11\)/); + assert.doesNotMatch(checkpointOutput, /bcrt1q[a-z0-9]+ \(m\/0\/12\)/); + break; + } + step++; + } else if (chunk.includes('Error:')) { + return respond(chunk); + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-cli/test/commands.test.ts b/packages/bitcore-cli/test/commands.test.ts index 39863cabf26..8ba05403e96 100644 --- a/packages/bitcore-cli/test/commands.test.ts +++ b/packages/bitcore-cli/test/commands.test.ts @@ -1,9 +1,9 @@ import { execSync } from 'child_process'; import assert from 'assert'; import { getCommands } from '../src/cli-commands'; -import { type IWallet } from '../types/wallet'; import { bitcoreLogo } from '../src/constants'; -import { type ICliOptions } from 'types/cli'; +import type { IWallet } from '../types/wallet'; +import type { ICliOptions } from '../types/cli'; describe('Option: --command', function() { const COMMANDS = getCommands({ wallet: {} as IWallet, opts: { command: 'any' } as ICliOptions }); diff --git a/packages/bitcore-cli/test/create.test.ts b/packages/bitcore-cli/test/create.test.ts new file mode 100644 index 00000000000..4b5415fc0b6 --- /dev/null +++ b/packages/bitcore-cli/test/create.test.ts @@ -0,0 +1,89 @@ +import { spawn } from 'child_process'; +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; +import { Transform } from 'stream'; +import * as helpers from './helpers'; + +describe('Create', function() { + const { KEYSTROKES, WALLETS: { CLI_EXEC, COMMON_OPTS, TEMP_DIR } } = helpers.CONSTANTS; + const commonOpts = [...COMMON_OPTS, '--dir', TEMP_DIR]; + + function cleanupTempWallets() { + if (fs.existsSync(TEMP_DIR)) { + fs.rmdirSync(TEMP_DIR, { recursive: true }); + } + } + + before(async function() { + cleanupTempWallets(); + await helpers.startBws(); + }); + + after(async function() { + await helpers.stopBws(); + }); + + describe('Single Sig', function() { + this.timeout(Math.max(this['_timeout'] || 0, 5000)); + const walletName = 'btc-temp'; + + it('should create a BTC wallet', function(done) { + const stepInputs = [ + [KEYSTROKES.ENTER], // Create Wallet + [KEYSTROKES.ENTER], // Chain: btc + ['regtest', KEYSTROKES.ENTER], // Network: regtest + [KEYSTROKES.ENTER], // Multi-party? No + [KEYSTROKES.ENTER], // Address Type: default + ['testpassword', KEYSTROKES.ENTER], // Password + [KEYSTROKES.ENTER], // View mnemonic + [':', 'q', KEYSTROKES.ENTER] // vim input to quit viewing mnemonic + ]; + let step = 0; + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n') || step == 7; + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + step++; + } else if (chunk.includes('Error:')) { + return respond(chunk); + } else if (chunk.endsWith(' created successfully!\n\n')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, walletName, ...commonOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + const wallet = JSON.parse(fs.readFileSync(path.join(TEMP_DIR, walletName + '.json'), 'utf-8')); + // Ensure that sensitive wallet key properties are encrypted and not present in plaintext + assert.ok(Object.hasOwn(wallet.key, 'mnemonicEncrypted')); + assert.ok(!Object.hasOwn(wallet.key, 'mnemonic')); + assert.ok(Object.hasOwn(wallet.key, 'xPrivKeyEncrypted')); + assert.ok(!Object.hasOwn(wallet.key, 'xPrivKey')); + assert.ok(Object.hasOwn(wallet.key, 'xPrivKeyEDDSAEncrypted')); + assert.ok(!Object.hasOwn(wallet.key, 'xPrivKeyEDDSA')); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-cli/test/data/addressesData.ts b/packages/bitcore-cli/test/data/addressesData.ts new file mode 100644 index 00000000000..b27d31e43c4 --- /dev/null +++ b/packages/bitcore-cli/test/data/addressesData.ts @@ -0,0 +1,228 @@ +export const addressesBtcSingleSig = [{ + _id: '69c2b226ff70dead7457114f', + version: '1.0.0', + createdOn: 1774367270, + address: 'bcrt1q6l953jevexkqrvvah8729nud289djcpae9jkx4', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/0', + publicKeys: [ + '0264f422ccd9234e65925549e1972a6fac2f61c67eedca999cf5661dc07116137d' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b226ff70dead74571151', + version: '1.0.0', + createdOn: 1774367270, + address: 'bcrt1qdq929kz9r7adapvruevgz0nkkqd3cpfvgh7wny', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/1', + publicKeys: [ + '02c00c3f4b6e6c86ff48b1208b7bb850884e18c851cc5e10b2e6d7eb4b319bd41a' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b309ff70dead74571153', + version: '1.0.0', + createdOn: 1774367497, + address: 'bcrt1qqr57cev8t25sph9qksdvslf80v9vy2nrlpwarz', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/2', + publicKeys: [ + '039f8e69d75f356544843d6eda9f9436839f503670ed864c1dd71a06dba4f23a42' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b30bff70dead74571155', + version: '1.0.0', + createdOn: 1774367499, + address: 'bcrt1quug3ztz5hgqe053hs2jzds70n0uynppu2lfyp3', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/3', + publicKeys: [ + '03d7bff943d7dd347366468006f50214301a1cdad5e411e33598c5aaee50fc7378' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b30dff70dead74571157', + version: '1.0.0', + createdOn: 1774367501, + address: 'bcrt1q0xp8938csu3rg9zxru7xfxer25ynzjzt3prh68', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/4', + publicKeys: [ + '02ba92eb873606022d5e55ab49f1cccc2e55976abdbd794f5b4e506533ee98a6df' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b30fff70dead74571159', + version: '1.0.0', + createdOn: 1774367503, + address: 'bcrt1qpn6lwuj30vdhjrl86pkxashmgf923c0jrv7vxc', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/5', + publicKeys: [ + '037775eb837a13743953da83f2360f09c69122012459e5af9d4b9ce154e532aa9a' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b311ff70dead7457115b', + version: '1.0.0', + createdOn: 1774367505, + address: 'bcrt1qqz5lc5wttuk2u5ntf0ptjjrpexs8n4upxg0hwe', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/6', + publicKeys: [ + '02357b4e1e5d381d95a549a50b7c8a4d174c7323a64e7ba0d18dfe49cb1a2c661e' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b313ff70dead7457115d', + version: '1.0.0', + createdOn: 1774367507, + address: 'bcrt1qdgv30yrsmlu790j40nm3mk895296va4xjyqar2', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/7', + publicKeys: [ + '02f36afa48b2ee30d6f56b2f612229968aad71c9c73450ae4e2a60f59175e73880' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b314ff70dead7457115f', + version: '1.0.0', + createdOn: 1774367508, + address: 'bcrt1q3s69dnlf2jnm50eaxxp2xyy8h5t7tah8yp354s', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/8', + publicKeys: [ + '0272d142747a7accd9e84353357b904284bc7834693f9f317999ace6875c1cfaac' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b317ff70dead74571161', + version: '1.0.0', + createdOn: 1774367511, + address: 'bcrt1qk93dstvzpyk5vpj9zt4gxzvsayuqvhkv6qws0e', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/9', + publicKeys: [ + '035415c5a0ae2965253cb06bbdd43cec2c05ae418b83cf14ee49a43e85ab30f6fd' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b319ff70dead74571163', + version: '1.0.0', + createdOn: 1774367513, + address: 'bcrt1qng4qgjrdqxx8n87pk5mnzvm2u6k3xjvgnt06zx', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/10', + publicKeys: [ + '0274f377b83bd1ac30a26ab6fbc0db90f97318f06c40142bb3d9112e9b554849d5' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}, +{ + _id: '69c2b31aff70dead74571165', + version: '1.0.0', + createdOn: 1774367514, + address: 'bcrt1q7kle0glqvheed9rykchzfs7nksfznnqygtn0my', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: false, + isEscrow: false, + path: 'm/0/11', + publicKeys: [ + '02f9896183b5226afb28ee54776f6c2513efaf2c36130b0271a330fa1b3d2a3b15' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: '', + beRegistered: true +}]; \ No newline at end of file diff --git a/packages/bitcore-cli/test/data/proposalsData.ts b/packages/bitcore-cli/test/data/proposalsData.ts new file mode 100644 index 00000000000..5693f41e80c --- /dev/null +++ b/packages/bitcore-cli/test/data/proposalsData.ts @@ -0,0 +1,123 @@ +export const btcSingleSigProposal = { + _id: '69c2edcf1351b13f22e61d7e', + type: null, + creatorName: '{"iv":"mRHROe2EFzG8ML8DJA==","v":1,"ts":128,"mode":"ccm","adata":"","cipher":"aes","ct":"OUsZkjyrsgCXfiesTtWihJtJ10O5zMg=","ks":128}', + createdOn: 1774382543, + id: 'e43b0fe2-c2d2-43c2-afaa-7fb28f212230', + txid: null, + txids: null, + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + creatorId: '90348b306bb58881013c63fe1238eddabf327836b2be808cdc9ebf97a426d9a5', + coin: 'btc', + chain: 'btc', + network: 'regtest', + message: null, + payProUrl: null, + from: null, + changeAddress: { + version: '1.0.0', + createdOn: 1774382543, + address: 'bcrt1q9nh7nzrcgzm96r4ms0mm9xvl3whfrucvdh0akr', + walletId: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + isChange: true, + isEscrow: false, + path: 'm/1/0', + publicKeys: [ + '020c1597c53bc5d61d6bf35a142bebebfdc38837c80648c981cae8ec234b02660f' + ], + coin: 'btc', + chain: 'btc', + network: 'regtest', + type: 'P2WPKH', + hasActivity: null, + beRegistered: null + }, + escrowAddress: null, + inputs: [ + { + address: 'bcrt1q6l953jevexkqrvvah8729nud289djcpae9jkx4', + satoshis: 100000000, + amount: 1, + scriptPubKey: '0014d7cb48cb2cc9ac01b19db9fca2cf8d51cad9603d', + txid: 'f07424a4f92c5be4f7f9ae1b065caded243d951c76b4d0eedc06e534927ac23c', + vout: 0, + locked: false, + confirmations: 3, + spent: false, + path: 'm/0/0', + publicKeys: [ + '0264f422ccd9234e65925549e1972a6fac2f61c67eedca999cf5661dc07116137d' + ] + } + ], + outputs: [ + { + amount: 12300000, + toAddress: 'bcrt1qdq929kz9r7adapvruevgz0nkkqd3cpfvgh7wny', + message: null + } + ], + outputOrder: [ + 1, + 0 + ], + walletM: 1, + walletN: 1, + requiredSignatures: 1, + requiredRejections: 1, + status: 'pending', + actions: [], + feeLevel: null, + feePerKb: 1000, + excludeUnconfirmedUtxos: false, + addressType: 'P2WPKH', + customData: null, + amount: 12300000, + fee: 141, + version: 3, + broadcastedOn: null, + inputPaths: [ + 'm/0/0' + ], + proposalSignature: '3045022100d88c317ce2f1577536cda7f276c38b76d84a6134ecb3f6de9c88ee7285906ce30220049d005c1bdbb4078733baa3d9d5c8416ee5b4e0bb83abac8dec24dc59abb002', + proposalSignaturePubKey: null, + proposalSignaturePubKeySig: null, + signingMethod: 'ecdsa', + lowFees: null, + raw: null, + nonce: null, + gasPrice: null, + maxGasFee: null, + priorityGasFee: null, + txType: null, + gasLimit: null, + data: null, + tokenAddress: null, + multisigContractAddress: null, + multisigTxId: null, + destinationTag: null, + invoiceID: null, + lockUntilBlockHeight: null, + instantAcceptanceEscrow: null, + isTokenSwap: null, + multiSendContractAddress: null, + enableRBF: null, + replaceTxByFee: null, + multiTx: null, + space: null, + nonceAddress: null, + blockHash: null, + blockHeight: null, + category: null, + priorityFee: null, + computeUnits: null, + memo: null, + fromAta: null, + decimals: null, + refreshOnPublish: null, + prePublishRaw: null, + derivationStrategy: 'BIP44', + isPending: true +}; + +btcSingleSigProposal['toObject'] = () => btcSingleSigProposal; \ No newline at end of file diff --git a/packages/bitcore-cli/test/data/test-config.ts b/packages/bitcore-cli/test/data/test-config.ts new file mode 100644 index 00000000000..e22eff05fb9 --- /dev/null +++ b/packages/bitcore-cli/test/data/test-config.ts @@ -0,0 +1,16 @@ +const host = process.env.DB_HOST || 'localhost'; +const port = process.env.DB_PORT || '27017'; +const dbname = 'cli_test'; + +const config = { + mongoDb: { + uri: `mongodb://${host}:${port}/${dbname}`, + dbname, + options: { useUnifiedTopology: true } + }, + bws: { + port: 4343 + } +}; + +export default config; diff --git a/packages/bitcore-cli/test/data/walletsData.ts b/packages/bitcore-cli/test/data/walletsData.ts new file mode 100644 index 00000000000..a1a7eebe42e --- /dev/null +++ b/packages/bitcore-cli/test/data/walletsData.ts @@ -0,0 +1,60 @@ +export const btcSingleSigWallet = { + _id: '6972a1648b48ae9c39b5e6c6', + version: '1.0.0', + createdOn: 1769120100, + id: '62e38685-f8e8-40d4-967f-04afc6aaf75a', + name: '{"iv":"1XcvvqJg/i9oMPz0TA==","v":1,"ts":128,"mode":"ccm","adata":"","cipher":"aes","ct":"Yjea/05D442crEePZ61547GHL0JJ3qM+hzyVUQQ=","ks":128}', + m: 1, + n: 1, + singleAddress: false, + status: 'complete', + publicKeyRing: [ + { + xPubKey: 'tpubDCKPndk1XFyHP68Znm9u2aAh337uzYuAskuRC8aN73iDeWjXtetsZg55exJkaAxq64wZkU8Ea5gsyazYM7ourdK4nQcZVF2kHYTwaNUsr7F', + requestPubKey: '03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79' + } + ], + copayers: [ + { + version: 2, + createdOn: 1769120100, + coin: 'btc', + chain: 'btc', + xPubKey: 'tpubDCKPndk1XFyHP68Znm9u2aAh337uzYuAskuRC8aN73iDeWjXtetsZg55exJkaAxq64wZkU8Ea5gsyazYM7ourdK4nQcZVF2kHYTwaNUsr7F', + id: '90348b306bb58881013c63fe1238eddabf327836b2be808cdc9ebf97a426d9a5', + name: '{"iv":"mRHROe2EFzG8ML8DJA==","v":1,"ts":128,"mode":"ccm","adata":"","cipher":"aes","ct":"OUsZkjyrsgCXfiesTtWihJtJ10O5zMg=","ks":128}', + requestPubKey: '03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79', + signature: '3044022057ea43cad38c3aba5922b25ec62d7c8842df51c49fc3711de5e8124527e5dbc102207353939f748defa938e33b6fa5d38a9a96a957ff37d741c3230533dd8d55787a', + requestPubKeys: [ + { + key: '03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79', + signature: '3044022057ea43cad38c3aba5922b25ec62d7c8842df51c49fc3711de5e8124527e5dbc102207353939f748defa938e33b6fa5d38a9a96a957ff37d741c3230533dd8d55787a' + } + ], + customData: '{"iv":"K8tACWgvaUzROguZbA==","v":1,"ts":128,"mode":"ccm","adata":"","cipher":"aes","ct":"fEpVWj5kkVr2bZAVvtaxAUQ3n1T3eMJ8oIOiu0pa9oV8Rxe020XRitJ0puBocm71ntNCjj3s32TYUaq40YKG80kCEgg2lmheG0mazLnISRHNBUFKF1//nMPfwa2Ya1zidRd+2A==","ks":128}' + } + ], + pubKey: '02665d4f16c446af06f4273bce80ff237f8c1eec77b710390722b4eded95a094ae', + coin: 'btc', + chain: 'btc', + network: 'regtest', + derivationStrategy: 'BIP44', + addressType: 'P2WPKH', + addressManager: { + version: 2, + derivationStrategy: 'BIP44', + receiveAddressIndex: 0, + changeAddressIndex: 0, + copayerIndex: 2147483647, + skippedPaths: [] + }, + scanStatus: '', + beRegistered: true, + beAuthPrivateKey2: '0c2738b1d810577777cd0db360335a75d217b1f3ab7841580acee5b8092a3d66', + beAuthPublicKey2: '0499fafc994b7a9461a337eaf38e054f55558a889f7846889373c36c285785fc4f9fbd041a8328fc6a6dd452750f92ae23fdac7c2fe761d3d1127ab307bfa846a2', + nativeCashAddr: '', + usePurpose48: false, + isShared: false +}; + +btcSingleSigWallet['toObject'] = () => btcSingleSigWallet; \ No newline at end of file diff --git a/packages/bitcore-cli/test/helpers.ts b/packages/bitcore-cli/test/helpers.ts new file mode 100644 index 00000000000..226a7d02113 --- /dev/null +++ b/packages/bitcore-cli/test/helpers.ts @@ -0,0 +1,233 @@ +import sinon from 'sinon'; +import assert from 'assert'; +import * as CWC from '@bitpay-labs/crypto-wallet-core'; +import BWS from '@bitpay-labs/bitcore-wallet-service'; +import { API, Constants } from '@bitpay-labs/bitcore-wallet-client'; +import { MongoClient } from 'mongodb'; +import supertest from 'supertest'; +import path from 'path'; +import util from 'util'; +import config from '../test/data/test-config'; +import type http from 'http'; + +const Bitcore = CWC.BitcoreLib; +const Bitcore_ = { + btc: CWC.BitcoreLib, + bch: CWC.BitcoreLibCash +}; +const { ExpressApp, Storage } = BWS; + +let client: MongoClient; +let expressApp: InstanceType; +let server: http.Server; +let storage: InstanceType; + +export const CONSTANTS = { + WALLETS: { + PASSWORD: 'testpassword', + CLI_EXEC: 'build/src/cli.js', + DIR: path.join(__dirname, './wallets'), + TEMP_DIR: path.join(__dirname, './wallets/temp'), + COMMON_OPTS: ['--verbose', '--host', `http://localhost:${config.bws.port}`], + BTC: { + SINGLE_SIG: 'btc-singlesig', + MULTI_SIG: 'btc-multisig', + THRESHOLD_SIG: 'btc-tss', + }, + }, + KEYSTROKES: { + ENTER: '\r', // Enter/Return + ARROW_UP: '\x1b[A', // Arrow Up + ARROW_DOWN: '\x1b[B', // Arrow Down + ARROW_RIGHT: '\x1b[C', // Arrow Right + ARROW_LEFT: '\x1b[D', // Arrow Left + DELETE: '\x1b[3~', // Delete + BACKSPACE: '\x7f', // Backspace + CTRL_C: '\x03', // Ctrl+C + } +}; + +export async function newDb() { + client = await MongoClient.connect(config.mongoDb.uri, config.mongoDb.options); + const db = client.db(config.mongoDb.dbname); + await db.dropDatabase(); + return { client, db }; +} + +export async function startBws() { + const { db } = await newDb(); + storage = new Storage({ db }); + Storage.createIndexes(db); + expressApp = new ExpressApp(); + return new Promise<{ storage: InstanceType }>(resolve => { + expressApp.start( + { + ignoreRateLimiter: true, + storage: storage, + blockchainExplorer: blockchainExplorerMock, + disableLogs: true, + doNotCheckV8: true + }, + () => { + sinon.stub(API.prototype, 'constructor').callsFake(function(opts) { + opts.request = supertest(expressApp.app); + return (API.prototype.constructor as any).wrappedMethod.call(API.prototype, opts); + }); + server = expressApp.app.listen(config.bws.port); + resolve({ storage }); + } + ); + }); +} + +export async function stopBws() { + return new Promise(resolve => { + (API.prototype.constructor as any).restore(); + expressApp.app.removeAllListeners(); + server.close(); + client.close(false, resolve); + }); +}; + +export async function loadWalletData(wallet: any) { + await util.promisify(storage.storeWalletAndUpdateCopayersLookup).call(storage, wallet); +} + +export async function loadWalletProposalData(proposal: any) { + await util.promisify(storage.storeTx).call(storage, proposal.walletId, proposal); +} + +export async function loadWalletAddressData(wallet: any, addresses: any[]) { + await util.promisify(storage.storeAddressAndWallet).call( + storage, + wallet, + addresses.map((a, i) => ({ ...a, createdOn: Math.floor(Date.now() / 1000) + i })) + ); +} + +export const blockchainExplorerMock = { + register: sinon.stub().callsArgWith(1, null, null), + getCheckData: sinon.stub().callsArgWith(1, null, { sum: 100 }), + addAddresses: sinon.stub().callsArgWith(2, null, null), + utxos: [], + lastBroadcasted: null, + txHistory: [], + feeLevels: [], + getUtxos: (wallet, height, cb) => { + return cb(null, JSON.parse(JSON.stringify(blockchainExplorerMock.utxos))); + }, + getAddressUtxos: (address, height, cb) => { + const selected = blockchainExplorerMock.utxos.filter(utxo => { + return address.includes(utxo.address); + }); + + return cb(null, JSON.parse(JSON.stringify(selected))); + }, + setUtxo: (address, amount, m, confirmations?) => { + const B = Bitcore_[address.coin]; + let scriptPubKey; + switch (address.type) { + case Constants.SCRIPT_TYPES.P2SH: + scriptPubKey = address.publicKeys ? B.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut() : ''; + break; + case Constants.SCRIPT_TYPES.P2WPKH: + case Constants.SCRIPT_TYPES.P2PKH: + scriptPubKey = B.Script.buildPublicKeyHashOut(address.address); + break; + case Constants.SCRIPT_TYPES.P2WSH: + scriptPubKey = B.Script.buildWitnessV0Out(address.address); + break; + } + assert(!!scriptPubKey, 'scriptPubKey not defined'); + blockchainExplorerMock.utxos.push({ + txid: new Bitcore.crypto.Hash.sha256(Buffer.alloc(Math.random() * 100000)).toString('hex'), + outputIndex: 0, + amount: amount, + satoshis: amount * 1e8, + address: address.address, + scriptPubKey: scriptPubKey.toBuffer().toString('hex'), + confirmations: confirmations == null ? Math.floor(Math.random() * 100 + 1) : +confirmations + }); + }, + supportsGrouping: () => { + return false; + }, + getBlockchainHeight: cb => { + return cb(null, 1000); + }, + broadcast: (raw, cb) => { + blockchainExplorerMock.lastBroadcasted = raw; + + let hash; + try { + const tx = new Bitcore.Transaction(raw); + if (!tx.outputs.length) { + throw 'no bitcoin'; + } + hash = tx.id; + // btc/bch + return cb(null, hash); + } catch (e) { + // try eth + hash = CWC.Transactions.getHash({ + tx: raw[0], + chain: 'ETH' + }); + return cb(null, hash); + } + }, + setHistory: txs => { + blockchainExplorerMock.txHistory = txs; + }, + getTransaction: (txid, cb) => { + return cb(); + }, + getTransactions: (wallet, startBlock, cb) => { + let list = [].concat(blockchainExplorerMock.txHistory); + // -1 = mempool, always included in server' s v8.js + list = list.filter(x => { + return x.height >= startBlock || x.height == -1; + }); + return cb(null, list); + }, + getAddressActivity: (address, cb) => { + const activeAddresses = blockchainExplorerMock.utxos.map(u => u.address); + return cb(null, activeAddresses.includes(address)); + }, + setFeeLevels: levels => { + blockchainExplorerMock.feeLevels = levels; + }, + estimateFee: (nbBlocks, cb) => { + const levels = {}; + for (const nb of nbBlocks) { + const feePerKb = blockchainExplorerMock.feeLevels[nb]; + levels[nb] = typeof feePerKb === 'number' ? feePerKb / 1e8 : -1; + } + + return cb(null, levels); + }, + estimateFeeV2: (opts, cb) => { + return cb(null, 20000); + }, + estimatePriorityFee: (opts, cb) => { + return cb(null, 5000); + }, + estimateGas: (nbBlocks, cb) => { + return cb(null, '20000000000'); + }, + getBalance: (nbBlocks, cb) => { + return cb(null, { + unconfirmed: 0, + confirmed: 20000000000 * 5, + balance: 20000000000 * 5 + }); + }, + getTransactionCount: (addr, cb) => { + return cb(null, 0); + }, + reset: () => { + blockchainExplorerMock.utxos = []; + blockchainExplorerMock.txHistory = []; + blockchainExplorerMock.feeLevels = []; + } +}; \ No newline at end of file diff --git a/packages/bitcore-cli/test/proposals.test.ts b/packages/bitcore-cli/test/proposals.test.ts new file mode 100644 index 00000000000..f848e6996b0 --- /dev/null +++ b/packages/bitcore-cli/test/proposals.test.ts @@ -0,0 +1,527 @@ +import { spawn } from 'child_process'; +import assert from 'assert'; +import { Transform } from 'stream'; +import * as helpers from './helpers'; +import * as walletData from './data/walletsData'; +import * as proposalData from './data/proposalsData'; +import { Utils } from '../src/utils'; + +describe('Proposals', function() { + this.timeout(Math.max(this['_timeout'] || 0, 5000)); + const { KEYSTROKES, WALLETS } = helpers.CONSTANTS; + const { CLI_EXEC, COMMON_OPTS, DIR } = WALLETS; + const cmdOpts = [...COMMON_OPTS, '--dir', DIR]; + + before(async function() { + await helpers.startBws(); + await helpers.loadWalletData(walletData.btcSingleSigWallet); + }); + + after(async function() { + await helpers.stopBws(); + }); + + it('should show no pending proposals', function(done) { + const stepInputs = [ + [KEYSTROKES.ENTER], // Proposals + // Checkpoint1: Proposals view shows no more proposals + ['x'], // Close -- (checkpoint1) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([1]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + if (checkpoints.has(step)) { + // Assert proposals output contains expected info for no pending proposals + assert.match(checkpointOutput, /No more proposals/); + } + step++; + } else if (chunk.includes('Error:')) { + return respond(chunk); + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + describe('Pending Proposals', function() { + beforeEach(async function() { + await helpers.loadWalletProposalData(proposalData.btcSingleSigProposal); + }); + + it('should show 1 pending proposal', function(done) { + const stepInputs = [ + // Checkpoint1: Proposals option should show 1 pending proposal + [KEYSTROKES.ENTER], // Proposals -- (checkpoint1) + // Checkpoint2: Proposals view shows pending proposal + ['x'], // Close -- (checkpoint2) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([0, 1]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert there's an indication of a pending proposal in main menu + assert.ok(checkpointOutput.includes(`Proposals${Utils.colorText(' (1)', 'yellow')}`)); + break; + case Array.from(checkpoints)[1]: + const lines = checkpointOutput.split('\n'); + const startIdx = lines.findIndex(l => l.includes('ID: e43b0fe2-c2d2-43c2-afaa-7fb28f212230 ')); + assert.ok(startIdx > -1); + assert.ok(lines[startIdx + 2].includes('Chain: BTC')); + assert.ok(lines[startIdx + 3].includes('Network: Regtest')); + assert.ok(lines[startIdx + 4].includes('Amount: 0.123 BTC')); + assert.ok(lines[startIdx + 5].includes('Fee: 0.00000141 BTC')); + assert.ok(lines[startIdx + 6].includes('Total Amount: 0.12300141 BTC')); + assert.ok(lines[startIdx + 7].includes('Fee Rate: 1 sat/B')); + assert.ok(lines[startIdx + 8].includes('Status: pending')); + assert.ok(lines[startIdx + 9].includes('Creator: kjoseph')); + assert.ok(lines[startIdx + 10].includes('Created: Tue Mar 24 2026 16:02:23 EDT')); + assert.ok(lines[startIdx + 11].includes('---------------------------')); + assert.ok(lines[startIdx + 12].includes('Recipients:')); + assert.ok(lines[startIdx + 13].includes('→ bcrt1qdq929kz9r7adapvruevgz0nkkqd3cpfvgh7wny: 0.123 BTC')); + assert.ok(lines[startIdx + 14].includes('↲ bcrt1q9nh7nzrcgzm96r4ms0mm9xvl3whfrucvdh0akr (change - m/1/0)')); + assert.ok(lines[startIdx + 15].includes('---------------------------')); + assert.ok(lines[startIdx + 16].includes(Utils.colorText('Missing Signatures: 1', 'yellow'))); + break; + } + step++; + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + it('should accept 1 pending proposal', function(done) { + const stepInputs = [ + // Checkpoint1: Proposals option should show 1 pending proposal + [KEYSTROKES.ENTER], // Proposals -- (checkpoint1) + ['a'], // Accept + // Checkpoint2: Proposals view shows accepted proposal + ['x'], // Close -- (checkpoint2) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([0, 2]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert there's an indication of a pending proposal in main menu + // eslint-disable-next-line no-control-regex + assert.match(checkpointOutput, /Proposals\x1B\[33m \(1\)\x1B\[0m/); + break; + case Array.from(checkpoints)[1]: + assert.ok(checkpointOutput.includes(`Broadcasted txid: ${Utils.colorText('5ba5df9de6c7f6043de8ade09c2dab08a1fb60724320f8cb4f00c9df2ec73035', 'green')}`)); + break; + } + step++; + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + it('should reject 1 pending proposal', function(done) { + const stepInputs = [ + // Checkpoint1: Proposals option should show 1 pending proposal + [KEYSTROKES.ENTER], // Proposals -- (checkpoint1) + ['j'], // Reject + // Checkpoint2: Should prompt for rejection reason + ['This proposal sux', KEYSTROKES.ENTER], // Enter rejection reason -- (checkpoint2) + // Checkpoint3: Should show rejected proposal + ['x'], // Close -- (checkpoint3) + // Checkpoint4: Main menu should show no pending proposals + [KEYSTROKES.ENTER], // Proposals -- (checkpoint4) + // Checkpoint5: Should show no more proposals + ['x'], // Close -- (checkpoint5) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([0, 2, 3, 4, 5]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert there's an indication of a pending proposal in main menu + // eslint-disable-next-line no-control-regex + assert.match(checkpointOutput, /Proposals\x1B\[33m \(1\)\x1B\[0m/); + break; + case Array.from(checkpoints)[1]: + assert.match(checkpointOutput, /Enter rejection reason:/); + break; + case Array.from(checkpoints)[2]: + const lines = checkpointOutput.split('\n'); + const startIdx = lines.findIndex(l => l.includes('◆ Page Controls:')); + assert.ok(startIdx > -1); + assert.ok(lines[startIdx + 1].includes('r Print Raw Object')); + assert.ok(lines[startIdx + 2].includes('e Export')); + assert.ok(lines[startIdx + 3].includes('x Close')); + assert.ok(lines.findIndex(l => l.includes('n Next Page')) === -1); + assert.ok(lines.findIndex(l => l.includes('p Previous Page')) === -1); + assert.ok(lines.findIndex(l => l.includes('a Accept')) === -1); + assert.ok(lines.findIndex(l => l.includes('j Reject')) === -1); + assert.ok(lines.findIndex(l => l.includes('d Delete')) === -1); + break; + case Array.from(checkpoints)[3]: + // No pending proposals indicator + assert.match(checkpointOutput, /Proposals \(Get pending transaction proposals\)/); + break; + case Array.from(checkpoints)[4]: + assert.match(checkpointOutput, /No more proposals/); + break; + } + step++; + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + it('should delete 1 pending proposal', function(done) { + const stepInputs = [ + // Checkpoint1: Proposals option should show 1 pending proposal + [KEYSTROKES.ENTER], // Proposals -- (checkpoint1) + ['d'], // Delete + [KEYSTROKES.ENTER], // Delete + // Checkpoint2: Should ask for confirmation + [KEYSTROKES.ENTER], // Default: No -- (checkpoint2) + ['d'], // Delete + // Checkpoint3: Should ask for confirmation again + [KEYSTROKES.ARROW_LEFT, KEYSTROKES.ENTER], // No -> Yes -- (checkpoint3) + // Checkpoint4: Should show no more proposals + ['x'], // Close -- (checkpoint4) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([0, 3, 5, 6]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert there's an indication of a pending proposal in main menu + // eslint-disable-next-line no-control-regex + assert.match(checkpointOutput, /Proposals\x1B\[33m \(1\)\x1B\[0m/); + break; + case Array.from(checkpoints)[1]: + case Array.from(checkpoints)[2]: + assert.match(checkpointOutput, /Are you sure you want to delete proposal/); + break; + case Array.from(checkpoints)[3]: + assert.match(checkpointOutput, /Proposal e43b0fe2-c2d2-43c2-afaa-7fb28f212230 deleted/); + break; + } + step++; + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }); + + it('should navigate multiple pending proposals', function(done) { + const txp2 = { ...proposalData.btcSingleSigProposal, id: '2d7cb6e5-68b2-4791-bf9a-045bf0d34e06', _id: undefined }; + txp2['toObject'] = () => txp2; + helpers.loadWalletProposalData(txp2) + .then(() => { + const stepInputs = [ + // Checkpoint1: Proposals option should show 2 pending proposals + [KEYSTROKES.ENTER], // Proposals (2) -- (checkpoint1) + // Checkpoint2: Should show first proposal + ['n'], // Next Page + // Checkpoint3: Should show second proposal + ['p'], // Previous Page -- (checkpoint3) + // Checkpoint4: Should show first proposal again + ['d'], // Delete (first proposal) -- (checkpoint4) + // Checkpoint5: Should ask for confirmation + [KEYSTROKES.ARROW_LEFT, KEYSTROKES.ENTER], // No -> Yes -- (checkpoint5) + ['n'], // Next Page + ['a'], // Accept (second proposal) + // Checkpoint6: Should show txid + ['p'], // Previous Page -- (checkpoint6) + // Checkpoint7: Should show deleted proposal + ['x'], // Close -- (checkpoint7) + [KEYSTROKES.ARROW_UP], // Proposals -> Exit + [KEYSTROKES.ENTER], // Exit + ]; + let step = 0; + let checkpointOutput = ''; + // stepInputs indexes corresponding to checkpoints in test flow where we want to assert on CLI output + const checkpoints = new Set([0, 1, 2, 3, 4, 7, 8]); + const io = new Transform({ + encoding: 'utf-8', + transform(chunk, encoding, respond) { + chunk = chunk.toString(); + if (checkpoints.has(step)) { + checkpointOutput += chunk; + } else { + checkpointOutput = ''; + } + // Uncomment to see CLI output during test + // process.stdout.write(chunk); + + const isStep = chunk.endsWith('└\n'); + if (isStep) { + for (const input of stepInputs[step]) { + this.push(input); + } + const lines = checkpointOutput.split('\n'); + switch (step) { + default: + break; // no-op for non-checkpoint steps + case Array.from(checkpoints)[0]: + // Assert there's an indication of a pending proposal in main menu + // eslint-disable-next-line no-control-regex + assert.match(checkpointOutput, /Proposals\x1B\[33m \(2\)\x1B\[0m/); + checkpointOutput = ''; // reset output to avoid false positives in next checkpoints + break; + case Array.from(checkpoints)[1]: + case Array.from(checkpoints)[3]: + case Array.from(checkpoints)[6]: + assert.match(checkpointOutput, /ID: e43b0fe2-c2d2-43c2-afaa-7fb28f212230 /); + assert.doesNotMatch(checkpointOutput, /ID: 2d7cb6e5-68b2-4791-bf9a-045bf0d34e06 /); + const startIdx = lines.findIndex(l => l.includes('◆ Page Controls:')); + assert.ok(startIdx > -1); + assert.ok(lines[startIdx + 1].includes('n Next Page')); + assert.doesNotMatch(checkpointOutput, /p {2}Previous Page/); + if (step < Array.from(checkpoints)[6]) { + assert.ok(checkpointOutput.includes('Status: pending')); + } else { + assert.ok(checkpointOutput.includes('Status: deleted')); + } + checkpointOutput = ''; // reset output to avoid false positives in next checkpoints + break; + case Array.from(checkpoints)[2]: + assert.doesNotMatch(checkpointOutput, /ID: e43b0fe2-c2d2-43c2-afaa-7fb28f212230 /); + assert.match(checkpointOutput, /ID: 2d7cb6e5-68b2-4791-bf9a-045bf0d34e06 /); + assert.ok(checkpointOutput.includes('p Previous Page')); + assert.doesNotMatch(checkpointOutput, /p {2}Next Page/); + checkpointOutput = ''; // reset output to avoid false positives in next checkpoints + break; + case Array.from(checkpoints)[4]: + assert.match(checkpointOutput, /Are you sure you want to delete proposal/); + checkpointOutput = ''; // reset output to avoid false positives in next checkpoints + break; + case Array.from(checkpoints)[5]: + assert.ok(checkpointOutput.includes(`Broadcasted txid: ${Utils.colorText('5ba5df9de6c7f6043de8ade09c2dab08a1fb60724320f8cb4f00c9df2ec73035', 'green')}`)); + checkpointOutput = ''; // reset output to avoid false positives in next checkpoints + break; + } + step++; + } + if (chunk.includes('👋')) { + child.stdin.end(); // send EOF to child so it can exit cleanly + } + respond(); + } + }); + const child = spawn('node', [CLI_EXEC, WALLETS.BTC.SINGLE_SIG, ...cmdOpts]); + child.stderr.pipe(process.stderr); + child.stdout.pipe(io).pipe(child.stdin); + let err; + io.on('error', (e) => { + err = e; + }); + child.on('error', (e) => { + err = e; + }); + child.on('close', (code) => { + assert.ifError(err); + assert.equal(code, 0); + done(); + }); + }) + .catch(done); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-cli/test/wallets/btc-singlesig.json b/packages/bitcore-cli/test/wallets/btc-singlesig.json new file mode 100644 index 00000000000..c79cd4babd6 --- /dev/null +++ b/packages/bitcore-cli/test/wallets/btc-singlesig.json @@ -0,0 +1 @@ +{"key":{"xPrivKey":"xprv9s21ZrQH143K3UxJbdjoUtVLgoUHNDafRx9PX7DvyjczjtgznRkqkMmqiEJ2XeHnuJxqNCR93xwg3a169NMc9FiXoYdyrk4jZruDwCoxWeV","xPrivKeyEDDSA":"xprv9s21ZrQH143K4LDzPgFhCGd3qbeMAdGoPmVVX2Q9vzeDw12sEZMHtyuYv5j8hvq66EgY1ES2e6SwUPNEa9ZSQ91cEpEW2hJkNhi1ZAFR8zr","mnemonic":"grab soap kitchen suggest salt quiz slogan candy cash note general dove","version":1,"mnemonicHasPassphrase":false,"fingerPrint":"e4794b6b","fingerPrintEDDSA":"ec84e87d","compliantDerivation":true,"id":"70123710-2d71-42ce-b09c-e260dadf4631"},"credentials":{"coin":"btc","chain":"btc","network":"regtest","xPubKey":"tpubDCKPndk1XFyHP68Znm9u2aAh337uzYuAskuRC8aN73iDeWjXtetsZg55exJkaAxq64wZkU8Ea5gsyazYM7ourdK4nQcZVF2kHYTwaNUsr7F","requestPrivKey":"0b491780ba50f1cf42c2b0ad816a247d034293e0ae53eb47b0d27e59996dc5dd","requestPubKey":"03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79","copayerId":"90348b306bb58881013c63fe1238eddabf327836b2be808cdc9ebf97a426d9a5","publicKeyRing":[{"xPubKey":"tpubDCKPndk1XFyHP68Znm9u2aAh337uzYuAskuRC8aN73iDeWjXtetsZg55exJkaAxq64wZkU8Ea5gsyazYM7ourdK4nQcZVF2kHYTwaNUsr7F","requestPubKey":"03d001f4439c9901b61979591b75e919fcfb1ffe1bd152ce0b5131be4de5ec8f79"}],"m":1,"n":1,"personalEncryptingKey":"/idxpn6qYww42TFH1e+ATw==","copayerName":"kjoseph","account":0,"addressType":"witnesspubkeyhash","version":2,"rootPath":"m/44'/0'/0'","keyId":"70123710-2d71-42ce-b09c-e260dadf4631"}} \ No newline at end of file diff --git a/packages/bitcore-cli/tsconfig.prod.json b/packages/bitcore-cli/tsconfig.prod.json new file mode 100644 index 00000000000..3f8ca5782df --- /dev/null +++ b/packages/bitcore-cli/tsconfig.prod.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "test" + ] +} \ No newline at end of file diff --git a/packages/bitcore-wallet-client/src/index.ts b/packages/bitcore-wallet-client/src/index.ts index 780ed63710a..e1f47e506a8 100644 --- a/packages/bitcore-wallet-client/src/index.ts +++ b/packages/bitcore-wallet-client/src/index.ts @@ -19,6 +19,8 @@ export { Encryption } from './lib/common/encryption'; export type * as EncryptionTypes from './lib/common/encryption'; export { Utils } from './lib/common/utils'; export type * as UtilsTypes from './lib/common/utils'; +export { Constants } from './lib/common/constants'; +export type * as ConstantsTypes from './lib/common/constants'; export { Errors } from './lib/errors'; export * as TssKey from './lib/tsskey';