diff --git a/package-lock.json b/package-lock.json index af90f0c..8197613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,14 @@ "better-sqlite3": "^12.6.2", "clipboardy": "^5.1.0", "ink": "^6.6.0", + "qrcode": "^1.5.4", "react": "^19.2.4", "zod": "^4.3.6" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.0.10", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -35,14 +37,15 @@ "license": "MIT", "dependencies": { "@bitauth/libauth": "^3.1.0-next.8", - "@electrum-cash/application": "^0.2.2-development.11641378031", - "@electrum-cash/network": "^4.1.4", - "@electrum-cash/protocol": "^2.3.0-development.12185537348", - "@xo-cash/crypto": "0.0.1", - "@xo-cash/primitives": "0.0.1", - "@xo-cash/state": "0.0.1", - "@xo-cash/types": "0.0.1", - "@xo-cash/utils": "0.0.1", + "@electrum-cash/application": "^0.2.3-development.13424909069", + "@electrum-cash/network": "^4.2.2", + "@electrum-cash/protocol": "^2.3.1", + "@electrum-cash/servers": "^3.1.0", + "@xo-cash/crypto": "file:../crypto", + "@xo-cash/primitives": "file:../primitives", + "@xo-cash/state": "file:../state", + "@xo-cash/types": "file:../types", + "@xo-cash/utils": "file:../utils", "eventemitter3": "^5.0.1" }, "devDependencies": { @@ -110,7 +113,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@xo-cash/types": "0.0.1" + "@xo-cash/types": "0.0.1-development.13504604083" }, "devDependencies": { "@chalp/eslint-airbnb": "^1.3.0", @@ -130,9 +133,6 @@ "typescript": "^5.3.2", "typescript-eslint": "^8.53.1", "vitest": "^4.0.17" - }, - "engines": { - "node": ">=18.0.0" } }, "../types": { @@ -160,9 +160,6 @@ "typescript": "^5.3.2", "typescript-eslint": "^8.53.1", "vitest": "^4.0.17" - }, - "engines": { - "node": ">=18.0.0" } }, "node_modules/@alcalzone/ansi-tokenize": { @@ -723,6 +720,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -896,6 +903,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -992,6 +1008,96 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/code-excerpt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", @@ -1004,6 +1110,24 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -1051,6 +1175,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1084,6 +1217,12 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -1234,6 +1373,19 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1255,6 +1407,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -1571,6 +1732,18 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lossless-json": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz", @@ -1689,6 +1862,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -1710,6 +1919,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1719,6 +1937,15 @@ "node": ">=8" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/powershell-utils": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.0.tgz", @@ -1782,6 +2009,23 @@ "once": "^1.3.1" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1835,6 +2079,21 @@ "node": ">= 6" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1905,6 +2164,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2216,6 +2481,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -2275,6 +2546,97 @@ } } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", diff --git a/package.json b/package.json index d10c0b8..51154be 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,14 @@ "better-sqlite3": "^12.6.2", "clipboardy": "^5.1.0", "ink": "^6.6.0", + "qrcode": "^1.5.4", "react": "^19.2.4", "zod": "^4.3.6" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.0.10", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/src/services/app.ts b/src/services/app.ts index 3072f80..4c36349 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -17,6 +17,7 @@ import { EventEmitter } from '../utils/event-emitter.js'; // TODO: Remove this. Exists to hash the seed for database namespace. import { createHash } from 'crypto'; import { p2pkhTemplate } from '@xo-cash/templates'; +import { hexToBin } from '@bitauth/libauth'; export type AppEventMap = { 'invitation-added': Invitation; @@ -58,6 +59,8 @@ export class AppService extends EventEmitter { // Import the default P2PKH template await engine.importTemplate(p2pkhTemplate); + console.log('p2pkhTemplate', JSON.stringify(p2pkhTemplate.transactions, null, 2)); + // Set default locking parameters for P2PKH // To my knowledge, this doesnt generate any lockscript, so discovery of funds will not work automatically. // TODO: Add discovery for funds in the first index? Or until we return 0 TXs? @@ -77,6 +80,25 @@ export class AppService extends EventEmitter { applicationIdentifier: config.electrumApplicationIdentifier, }); + // TEMP because testing is painful + // Remove all reserved UTXOs on startup + // First, get every unspent output + const allUnspentOutputs = await engine.listUnspentOutputsData(); + + // Get a set of all the invitation identifiers + const allInvitationIdentifiers = new Set(allUnspentOutputs.map(output => output.invitationIdentifier)); + + // Iterate over the invitation identifiers and unreserve the outputs + for (const invitationIdentifier of allInvitationIdentifiers) { + // Get the outputs for the invitation + const outputs = allUnspentOutputs.filter(output => output.invitationIdentifier === invitationIdentifier); + // Unreserve the outputs + await engine.unreserveResources(outputs.map(output => ({ + outpointTransactionHash: hexToBin(output.outpointTransactionHash), + outpointIndex: output.outpointIndex, + })), invitationIdentifier); + } + return new AppService(engine, walletStorage, config, electrum); } diff --git a/src/services/history.ts b/src/services/history.ts index 41e2464..5ce12f6 100644 --- a/src/services/history.ts +++ b/src/services/history.ts @@ -68,6 +68,55 @@ export class HistoryService { private invitations: Invitation[] ) {} + async extractEntities(invitation: XOInvitation): Promise { + const entities = new Set(); + for (const commit of invitation.commits) { + entities.add(commit.entityIdentifier); + } + return Array.from(entities); + } + + // Entities are currently static per invitation. So, we can try to match the roles to entities by: + // Iterating through each commit, extract the entity into a Map. + // While we iterate through the commits, if we see a role declaration in the commit, we save that role onto the entity's roles array. + // After we have iterated through all the commits, we can return the Map. + async matchRolesToEntities(invitation: XOInvitation, entities: string[]): Promise> { + const entitiesMap = new Map>(); + for (const entity of entities) { + entitiesMap.set(entity, new Set()); + } + + // First pass, we are just going to try and find roleIdentifer values in the inputs, outputs, and variables. + // TODO: Update this once the invitations use XPubs + for (const commit of invitation.commits) { + const entity = commit.entityIdentifier; + const roles = entitiesMap.get(entity) ?? new Set(); + + for (const input of commit.data.inputs ?? []) { + if (input.roleIdentifier) roles.add(input.roleIdentifier); + } + for (const output of commit.data.outputs ?? []) { + if (output.roleIdentifier) roles.add(output.roleIdentifier); + } + for (const variable of commit.data.variables ?? []) { + if (variable.roleIdentifier) roles.add(variable.roleIdentifier); + } + } + + // TODO: We might be able to use the lockingBytecodes that we have generated to infer which role we are. But the templates dont tell us which role is responsible for a particular output. + // I.e, if we dont know what role an output was from, we cant match it using the lockingBytecode to a role. + // Example: 2 inputs to a TX for the same amount. We dont know whether we would be Sender1 or Sender2. + // So, for now we are just going to rely on the roleIdentifiers that we have found in the first pass. + + // Format into a record for easier access. + const entitiesRecord: Record = {}; + for (const [entity, roles] of entitiesMap.entries()) { + entitiesRecord[entity] = Array.from(roles); + } + + return entitiesRecord; + } + async getHistory(): Promise { const allUtxos = await this.engine.listUnspentOutputsData(); const ownOutpoints = new Set(); @@ -94,6 +143,10 @@ export class HistoryService { walletEntityIdentifier, }); this.indexInvitationOutputsByUtxoOrigin(invitationByOrigin, invitation); + + const entities = await this.extractEntities(invitation.data); + const entitiesRecord = await this.matchRolesToEntities(invitation.data, entities); + console.log(entitiesRecord); } const usedUtxoIds = new Set(); @@ -288,6 +341,13 @@ export class HistoryService { }; } + /** + * TODO: This is completely incorrect. It should be deriving the roles of the user based on the entity IDs in the commits, then mapping each commit to Inputs/Outputs so we know what belongs to the user. + * There are a few changes that will need to be made to make this work: + * 1. Provide a way to derive all entity IDs of the user for the invitation (If we are going with XPub) + * 2. Provide a way to get only the User's commits (and their inputs/outputs) + * 3. (Maybe) Include role on inputs and outputs - This one might be fine with just using the commit entity id + */ private deriveWalletRolesForInvitation( context: InvitationContext, outputs: HistoryUtxoItem[] diff --git a/src/services/invitation.ts b/src/services/invitation.ts index 5aad491..dc7dd08 100644 --- a/src/services/invitation.ts +++ b/src/services/invitation.ts @@ -412,7 +412,7 @@ export class Invitation extends EventEmitter { await this.syncServer.publishInvitation(this.data); } - async findSuitableResources(options: FindSuitableResourcesParameters): Promise { + async findSuitableResources(options: Partial = {}): Promise { // Find the suitable resources const { unspentOutputs } = await this.engine.findSuitableResources(this.data, options); diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 59b37f2..f55cbf9 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -80,8 +80,12 @@ function StatusBar(): React.ReactElement { function DialogOverlay(): React.ReactElement | null { const { dialog, setDialog } = useDialog(); + // 'custom' dialogs are rendered and managed by the screen itself; + // we only handle input for the built-in dialog types. + const isBuiltInDialog = dialog?.visible === true && dialog.type !== 'custom'; + useInput((input, key) => { - if (!dialog?.visible) return; + if (!isBuiltInDialog) return; if (key.return || input === 'y' || input === 'Y') { if (dialog.type === 'confirm' && dialog.onConfirm) { @@ -92,9 +96,9 @@ function DialogOverlay(): React.ReactElement | null { } else if (key.escape || input === 'n' || input === 'N') { dialog.onCancel?.(); } - }, { isActive: dialog?.visible ?? false }); + }, { isActive: isBuiltInDialog }); - if (!dialog?.visible) return null; + if (!isBuiltInDialog) return null; const borderColor = dialog.type === 'error' ? colors.error : dialog.type === 'confirm' ? colors.warning : diff --git a/src/tui/components/QRCode.tsx b/src/tui/components/QRCode.tsx new file mode 100644 index 0000000..e437abd --- /dev/null +++ b/src/tui/components/QRCode.tsx @@ -0,0 +1,198 @@ +/** + * QR Code component for displaying scannable QR codes in the terminal. + * + * Uses the lower-half-block character (▄) exclusively for rendering. The top + * half of each cell is controlled via backgroundColor and the bottom half via + * the foreground color. This avoids the sub-pixel seams that occur when mixing + * different Unicode block characters (█, ▀, ▄, space) across adjacent rows. + */ + +import React, { useMemo } from 'react'; +import { Box, Text } from 'ink'; +import QRCodeLib from 'qrcode'; +import { DialogWrapper } from './Dialog.js'; +import { colors } from '../theme.js'; + +/** Color used for light (background) QR modules. */ +const LIGHT = 'white'; + +/** Color used for dark (data) QR modules. Must match the dialog/terminal bg. */ +const DARK = colors.bg as string; + +/** Default quiet zone size in modules (QR spec recommends 4, 2 is usually sufficient). */ +const QUIET_ZONE = 2; + +/** + * A run of consecutive characters in a rendered QR row that share the + * same foreground/background color pair. + */ +interface ColorSpan { + /** The repeated ▄ characters for this span. */ + chars: string; + /** Foreground color (controls the bottom half of each cell). */ + fg: string; + /** Background color (controls the top half of each cell). */ + bg: string; +} + +/** + * Props for the QRCode component. + */ +interface QRCodeProps { + /** The data to encode in the QR code. */ + value: string; + /** Whether to wrap the QR code in a DialogWrapper. */ + dialog?: boolean; + /** Dialog title (only used when dialog is true). Defaults to "QR Code". */ + dialogTitle?: string; + /** Whether to display the raw encoded value as copyable text above the QR code. */ + showValue?: boolean; +} + +/** + * Generates the QR code module matrix with a quiet zone border. + * + * @param value - The string to encode. + * @param quietZone - Number of light-module rows/columns to add around the QR data. + * @returns A 2D array where `true` means dark module and `false` means light module. + */ +function generateMatrix(value: string, quietZone: number = QUIET_ZONE): boolean[][] { + const qr = QRCodeLib.create(value, { errorCorrectionLevel: 'M' }); + const { size, data } = qr.modules; + const totalSize = size + quietZone * 2; + + const matrix: boolean[][] = []; + + for (let row = 0; row < totalSize; row++) { + const matrixRow: boolean[] = []; + + for (let col = 0; col < totalSize; col++) { + const qrRow = row - quietZone; + const qrCol = col - quietZone; + const insideData = qrRow >= 0 && qrRow < size && qrCol >= 0 && qrCol < size; + + // Quiet zone modules are always light (false). + matrixRow.push(insideData ? data[qrRow * size + qrCol] === 1 : false); + } + + matrix.push(matrixRow); + } + + return matrix; +} + +/** + * Converts a pair of module rows into an array of {@link ColorSpan}s. + * + * Every cell uses the `▄` (lower half block) character. The foreground color + * paints the bottom half and the backgroundColor paints the top half, giving + * us artifact-free rendering with a single glyph. + * + * Consecutive cells that share the same color pair are merged into one span + * to keep the element count low. + * + * @param matrix - The full module matrix. + * @param row - The index of the top row in the pair (the bottom row is row + 1). + * @returns An array of color spans for this terminal line. + */ +function buildRowSpans(matrix: boolean[][], row: number): ColorSpan[] { + const width = matrix[0]?.length ?? 0; + const spans: ColorSpan[] = []; + + for (let col = 0; col < width; col++) { + const topDark = matrix[row]?.[col] ?? false; + const bottomDark = matrix[row + 1]?.[col] ?? false; + + // ▄ lower-half block: foreground = bottom color, backgroundColor = top color + const fg = bottomDark ? DARK : LIGHT; + const bg = topDark ? DARK : LIGHT; + + const last = spans[spans.length - 1]; + if (last && last.fg === fg && last.bg === bg) { + last.chars += '▄'; + } else { + spans.push({ chars: '▄', fg, bg }); + } + } + + return spans; +} + +/** + * Renders the full module matrix into an array of span-arrays, one per + * terminal row (each covering two QR module rows). + * + * @param matrix - The 2D dark/light module matrix from {@link generateMatrix}. + */ +function renderMatrix(matrix: boolean[][]): ColorSpan[][] { + const rows: ColorSpan[][] = []; + const height = matrix.length; + + for (let row = 0; row < height; row += 2) { + rows.push(buildRowSpans(matrix, row)); + } + + return rows; +} + +/** + * Displays a scannable QR code in the terminal. + * + * Supports optional dialog wrapping via the `dialog` prop and an optional + * copyable text display of the encoded value via `showValue`. + * + * @example + * ```tsx + * // Minimal usage + * + * + * // Inside a dialog with the raw value shown + * + * ``` + */ +export function QRCode({ + value, + dialog = false, + dialogTitle = 'QR Code', + showValue = false, +}: QRCodeProps): React.ReactElement { + const { rows, moduleCount } = useMemo(() => { + const matrix = generateMatrix(value); + return { + rows: renderMatrix(matrix), + moduleCount: matrix[0]?.length ?? 0, + }; + }, [value]); + + const qrContent = ( + + {showValue && ( + + {value} + + )} + + {rows.map((spans, i) => ( + + {spans.map((span, j) => ( + + {span.chars} + + ))} + + ))} + + + ); + + if (dialog) { + const dialogWidth = Math.max(moduleCount + 8, 40); + return ( + + {qrContent} + + ); + } + + return qrContent; +} diff --git a/src/tui/components/index.ts b/src/tui/components/index.ts index f3bc7c7..ac25d49 100644 --- a/src/tui/components/index.ts +++ b/src/tui/components/index.ts @@ -16,3 +16,4 @@ export { } from './List.js'; export { InputDialog, ConfirmDialog, MessageDialog } from './Dialog.js'; export { ProgressBar, StepIndicator, Loading, type Step } from './ProgressBar.js'; +export { QRCode } from './QRCode.js'; diff --git a/src/tui/screens/SeedInput.tsx b/src/tui/screens/SeedInput.tsx index b6b0a88..5b5649d 100644 --- a/src/tui/screens/SeedInput.tsx +++ b/src/tui/screens/SeedInput.tsx @@ -55,10 +55,7 @@ function loadMnemonicFiles(): MnemonicFileEntry[] { const parsed = BCHMnemonicURL.fromURL(content); const raw = parsed.toObject(); - console.log(raw); - const mnemonicResult = encodeBip39Mnemonic(raw.entropy); - console.log(mnemonicResult); if (typeof mnemonicResult === 'string') continue; /** Use the URL comment as the label, falling back to a cleaned-up filename. */ diff --git a/src/tui/screens/WalletState.tsx b/src/tui/screens/WalletState.tsx index 49156c6..326bb5f 100644 --- a/src/tui/screens/WalletState.tsx +++ b/src/tui/screens/WalletState.tsx @@ -10,12 +10,14 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Box, Text, useInput } from 'ink'; import { ScrollableList, type ListItemData } from '../components/List.js'; +import { QRCode } from '../components/QRCode.js'; import { useNavigation } from '../hooks/useNavigation.js'; -import { useAppContext, useStatus } from '../hooks/useAppContext.js'; +import { useAppContext, useStatus, useDialog } from '../hooks/useAppContext.js'; import { colors, formatSatoshis, formatHex, logoSmall } from '../theme.js'; import type { HistoryItem } from '../../services/history.js'; import { generateTemplateIdentifier } from '@xo-cash/engine'; +import { hexToBin, lockingBytecodeToCashAddress } from '@bitauth/libauth'; // Import utility functions import { @@ -71,6 +73,7 @@ export function WalletStateScreen(): React.ReactElement { const { navigate } = useNavigation(); const { appService, showError, showInfo } = useAppContext(); const { setStatus } = useStatus(); + const { setDialog } = useDialog(); // State const [balance, setBalance] = useState<{ totalSatoshis: bigint; utxoCount: number } | null>(null); @@ -80,6 +83,9 @@ export function WalletStateScreen(): React.ReactElement { const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0); const [isLoading, setIsLoading] = useState(true); + /** Cash address to display in the QR code dialog (null when dialog is hidden). */ + const [qrAddress, setQrAddress] = useState(null); + /** * Refreshes wallet state. */ @@ -122,7 +128,7 @@ export function WalletStateScreen(): React.ReactElement { }, [refresh]); /** - * Generates a new receiving address. + * Generates a new receiving address and displays it as a QR code. */ const generateNewAddress = useCallback(async () => { if (!appService) { @@ -145,21 +151,33 @@ export function WalletStateScreen(): React.ReactElement { // Generate the template identifier const templateId = generateTemplateIdentifier(p2pkhTemplate); - // Generate the locking bytecode - const lockingBytecode = await appService.engine.generateLockingBytecode( + // Generate the locking bytecode (returned as a hex string) + const lockingBytecodeHex = await appService.engine.generateLockingBytecode( templateId, 'receiveOutput', 'receiver', ); - showInfo(`New address generated!\n\nLocking bytecode:\n${formatHex(lockingBytecode, 40)}`); + // Convert the locking bytecode to a BCH cash address for display and QR encoding. + const result = lockingBytecodeToCashAddress({ bytecode: hexToBin(lockingBytecodeHex), prefix: 'bitcoincash' }); + + if (typeof result === 'string') { + showError(`Failed to encode address: ${result}`); + return; + } + + console.log(result); + + setQrAddress(result.address); + setDialog({ visible: true, type: 'custom', message: '' }); + setStatus('Address generated'); // Refresh to show updated state await refresh(); } catch (error) { showError(`Failed to generate address: ${error instanceof Error ? error.message : String(error)}`); } - }, [appService, setStatus, showInfo, showError, refresh]); + }, [appService, setStatus, showError, refresh]); /** * Handles menu action. @@ -209,8 +227,16 @@ export function WalletStateScreen(): React.ReactElement { }); }, [history]); - // Handle keyboard navigation between panels + // Handle keyboard navigation between panels and QR dialog dismissal useInput((input, key) => { + if (qrAddress) { + if (key.escape || key.return) { + setQrAddress(null); + setDialog(null); + } + return; + } + if (key.tab) { setFocusedPanel(prev => prev === 'menu' ? 'history' : 'menu'); } @@ -400,6 +426,26 @@ export function WalletStateScreen(): React.ReactElement { Tab: Switch focus • Enter: Select • ↑↓: Navigate • Esc: Back + + {/* QR Code dialog overlay for generated addresses */} + {qrAddress && ( + + + + Press Enter or Esc to close + + + )} ); } diff --git a/src/tui/screens/action-wizard/ActionWizardScreen.tsx b/src/tui/screens/action-wizard/ActionWizardScreen.tsx index ae97d2b..1697599 100644 --- a/src/tui/screens/action-wizard/ActionWizardScreen.tsx +++ b/src/tui/screens/action-wizard/ActionWizardScreen.tsx @@ -1,149 +1,22 @@ import React from 'react'; -import { Box, Text, useInput } from 'ink'; +import { Box, Text } from 'ink'; import { StepIndicator, type Step } from '../../components/ProgressBar.js'; import { Button } from '../../components/Button.js'; import { colors, logoSmall } from '../../theme.js'; -import { useActionWizard } from './useActionWizard.js'; +import { useActionWizard } from './hooks/useActionWizard.js'; +import { useWizardKeyboard } from './hooks/useWizardKeyboard.js'; // Steps -import { InfoStep } from './steps/InfoStep.js'; import { RoleSelectStep } from './steps/RoleSelectStep.js'; import { VariablesStep } from './steps/VariablesStep.js'; import { InputsStep } from './steps/InputsStep.js'; import { ReviewStep } from './steps/ReviewStep.js'; import { PublishStep } from './steps/PublishStep.js'; +import { DataResultStep } from './steps/DataResultStep.js'; export function ActionWizardScreen(): React.ReactElement { const wizard = useActionWizard(); - - // ── Keyboard handling ────────────────────────────────────────── - useInput( - (input, key) => { - // Tab to cycle between content area and button bar - if (key.tab) { - if (wizard.focusArea === 'content') { - // Within the role-select step, tab through roles first - if ( - wizard.currentStepData?.type === 'role-select' && - wizard.availableRoles.length > 0 - ) { - if ( - wizard.selectedRoleIndex < - wizard.availableRoles.length - 1 - ) { - wizard.setSelectedRoleIndex((prev) => prev + 1); - return; - } - } - // Within the inputs step, tab through UTXOs first - if ( - wizard.currentStepData?.type === 'inputs' && - wizard.availableUtxos.length > 0 - ) { - if ( - wizard.selectedUtxoIndex < - wizard.availableUtxos.length - 1 - ) { - wizard.setSelectedUtxoIndex((prev) => prev + 1); - return; - } - } - // Move focus down to the button bar - wizard.setFocusArea('buttons'); - wizard.setFocusedButton('next'); - } else { - // Cycle through buttons, then wrap back to content - if (wizard.focusedButton === 'back') { - wizard.setFocusedButton('cancel'); - } else if (wizard.focusedButton === 'cancel') { - wizard.setFocusedButton('next'); - } else { - wizard.setFocusArea('content'); - wizard.setFocusedInput(0); - wizard.setSelectedUtxoIndex(0); - wizard.setSelectedRoleIndex(0); - } - } - return; - } - - // Arrow keys for role selection in the content area - if ( - wizard.focusArea === 'content' && - wizard.currentStepData?.type === 'role-select' - ) { - if (key.upArrow) { - wizard.setSelectedRoleIndex((p) => Math.max(0, p - 1)); - } else if (key.downArrow) { - wizard.setSelectedRoleIndex((p) => - Math.min(wizard.availableRoles.length - 1, p + 1) - ); - } - return; - } - - // Arrow keys for UTXO selection in the content area - if ( - wizard.focusArea === 'content' && - wizard.currentStepData?.type === 'inputs' - ) { - if (key.upArrow) { - wizard.setSelectedUtxoIndex((p) => Math.max(0, p - 1)); - } else if (key.downArrow) { - wizard.setSelectedUtxoIndex((p) => - Math.min(wizard.availableUtxos.length - 1, p + 1) - ); - } else if (key.return || input === ' ') { - wizard.toggleUtxoSelection(wizard.selectedUtxoIndex); - } - return; - } - - // Arrow keys in button bar - if (wizard.focusArea === 'buttons') { - if (key.leftArrow) { - wizard.setFocusedButton((p) => - p === 'next' ? 'cancel' : p === 'cancel' ? 'back' : 'back' - ); - } else if (key.rightArrow) { - wizard.setFocusedButton((p) => - p === 'back' ? 'cancel' : p === 'cancel' ? 'next' : 'next' - ); - } - - // Enter on a button - if (key.return) { - if (wizard.focusedButton === 'back') wizard.previousStep(); - else if (wizard.focusedButton === 'cancel') wizard.cancel(); - else if (wizard.focusedButton === 'next') wizard.nextStep(); - } - } - - // 'c' to copy invitation ID on the publish step - if ( - input === 'c' && - wizard.currentStepData?.type === 'publish' && - wizard.invitationId - ) { - wizard.copyId(); - } - - // 'a' to select all UTXOs - if (input === 'a' && wizard.currentStepData?.type === 'inputs') { - wizard.setAvailableUtxos((p) => - p.map((u) => ({ ...u, selected: true })) - ); - } - - // 'n' to deselect all UTXOs - if (input === 'n' && wizard.currentStepData?.type === 'inputs') { - wizard.setAvailableUtxos((p) => - p.map((u) => ({ ...u, selected: false })) - ); - } - }, - { isActive: !wizard.textInputHasFocus } - ); + useWizardKeyboard(wizard); // ── Step router ──────────────────────────────────────────────── const renderStep = () => { @@ -152,15 +25,6 @@ export function ActionWizardScreen(): React.ReactElement { } switch (wizard.currentStepData?.type) { - case 'info': - return ( - - ); case 'role-select': return ( ); + case 'result': + return ( + + ); default: return null; } @@ -278,7 +150,7 @@ export function ActionWizardScreen(): React.ReactElement { wizard.focusArea === "buttons" && wizard.focusedButton === "back" } - disabled={wizard.currentStepData?.type === "publish"} + disabled={wizard.isLastStep} />